home *** CD-ROM | disk | FTP | other *** search
Text File | 1996-02-01 | 628.5 KB | 14,040 lines |
- File: PCTIM003.TXT
- Description: FAQ / Application notes: Timing on the PC family under DOS
- Author: Kris Heidenstrom (kheidens@actrix.gen.nz)
- Version: 19951220, Release 3
-
- --------------------------------------------------------------------------------
-
- ## 1 INTRODUCTION AND DOCUMENT INFORMATION
-
- ## 1.1 DOCUMENT OVERVIEW
-
- This article describes techniques for timing on the IBM PC family under MS-DOS,
- and many related subjects. Sample functions and programs are included. After
- the brief overview, the features of each technique are listed, so you can find
- the most appropriate one for your needs. Subjects covered in this document
- include:
-
- ■ The DOS and BIOS date/time and alarm functions
- ■ The BIOS tick count variable
- ■ Trapping and handling critical errors
- ■ Using interrupt 1C hex and interrupt 8
- ■ The counter/timer's internal operation
- ■ Reprogramming the timer operating mode
- ■ Measuring short time intervals (three techniques)
- ■ Reading the timer count in progress
- ■ Generating an absolute timestamp
- ■ Reprogramming the timer tick rate
- ■ Simulating a vertical retrace interrupt for triple buffering
- ■ Using the serial and parallel port interrupts
- ■ Reading the joystick position (three methods)
- ■ Generating tones and sound.
-
- In addition to these timing techniques, this document covers the PC's timing
- hardware, and covers interrupts and interrupt considerations in some detail.
-
- Also included in this package is an archive containing executable versions of
- the sample programs, and an archive containing six illustrations in GIF format.
-
- ## 1.1.1 AUDIENCE
-
- This document is not aimed at programmers who wear suits and write database
- query programs in Cobol. It is aimed at the 'tinkerer' programmer or low-level
- programmer, who wants complete control of the computer, wants to work closely
- with the hardware, and who is familiar with, and interested in, real time
- concepts. Previous programming experience in C and assembly language, and
- familiarity with DOS and BIOS design, would be an advantage.
-
- ## 1.2 CONTENTS
-
- 1 INTRODUCTION AND DOCUMENT INFORMATION
- 1.1 Document Overview
- 1.1.1 Audience
- 1.2 Contents
- 1.3 Author and Distribution
- 1.4 Disclaimer and Legal stuff
- 1.5 Document Conventions
- 1.6 Sample Code Conventions
- 1.7 Acknowledgements
- 1.8 Quoter Program
- 1.9 Revision notes
- 1.10 Glossary
- 2 OVERVIEW OF TIMING TECHNIQUES
- 2.1 The Big Picture
- 2.2 Which Technique?
- 2.3 Comparison of Techniques
- 2.4 Other Subjects Covered in this Document
- 3 DOS AND BIOS TIME-OF-DAY AND ALARM FUNCTIONS
- 3.1 Reading the Date and Time from DOS
- 3.2 Reading the Date and Time from the BIOS
- 3.3 Sample Program: DOS Device Driver for the AT Clock
- 3.4 Other BIOS Time and Alarm Functions
- 3.5 Other Other BIOS Time Functions
- 3.6 The Times They Are A-Changin'
- 4 USING THE BIOS TICK COUNT VARIABLE
- 4.1 The BIOS Tick Count Variable
- 4.2 Change of Day
- 4.3 Reading and Setting the Tick Count
- 4.4 Special Requirements - None
- 4.5 Sample Program: Reading the Tick Count
- 4.6 Sample Code: Optimised Function to Read the Tick Count
- 4.7 Sample Program: Using the Tick Count for Timeout Checking
- 4.8 Simple Delays using the BIOS Tick Count
- 5 SPECIAL SOFTWARE PRECAUTIONS
- 5.1 The Ctrl-C and Ctrl-Break Interrupts
- 5.2 Handling the Ctrl-C Interrupt
- 5.3 The Critical Error Interrupt
- 5.4 Critical Error Handler Parameters
- 5.5 Critical Error Handler Operation
- 5.6 The Divide Overflow Interrupt
- 5.7 Error Handling System
- 5.8 Sample Code Module: Critical Error Handler module
- 6 INTERRUPTS
- 6.1 The Timer Tick Interrupts
- 6.2 Interrupt Vector Table
- 6.3 Intercepting an Interrupt
- 6.4 Interrupt Hardware
- 6.5 IRQ to Interrupt Mapping
- 6.6 Interrupt Flag, Interrupt Acceptance, Interrupt Nesting
- 6.7 EMM386 Interrupt Interception
- 6.8 Avoiding EMM386 Overhead
- 6.9 Long Timer Tick Interrupt Handlers
- 6.9.1 Danger of Long Timer Tick Interrupt Handlers
- 6.10 Interrupt Mask Register
- 6.11 Enabling and Disabling the Timer Tick Interrupt
- 6.12 Reading the Interrupt Request Register
- 6.13 Reading the Interrupt In Service Register
- 6.14 When You Should Disable Interrupts
- 6.15 When You Shouldn't Disable Interrupts
- 6.16 Causes of Interrupt Delivery Jitter and Fast Tick Loss
- 6.16.1 Interrupt Delivery Jitter due to Real Interrupts
- 6.16.2 Interrupt Delivery Jitter due to Software Interrupts
- 6.16.3 Interrupt Delivery Jitter due to Hardware Accesses
- 6.16.4 Avoiding Interrupt Delivery Jitter
- 6.17 Detecting Interrupt Delivery Jitter and Missed Fast Tick Interrupts
- 6.18 Disabling Interrupts for Longer than One Timer Tick
- 6.19 Disabling Interrupts for Long Periods of Time
- 6.20 Overhead of an Interrupt
- 6.21 Effect of Background Interrupts
- 6.22 Safe Control of Interrupts
- 6.23 Timer Tick Interrupt Handler Guidelines
- 6.24 Accessing Hardware Devices in an Interrupt Handler
- 6.25 Calling DOS and BIOS in an Interrupt Handler
- 6.26 Calling C Library Functions in an Interrupt Handler
- 6.27 Re-entry of Interrupt Handlers
- 6.28 The 'End Of Interrupt' Signal
- 6.28.1 Level-Triggered Interrupt Reset
- 6.29 Enabling and Disabling Interrupts in an Interrupt Handler
- 6.30 Stack Usage and Stack Checking in an Interrupt Handler
- 6.31 Chaining to the Old Interrupt Handler
- 6.32 Writing Interrupt Handlers in Assembly Language
- 6.32.1 Assembly Language Interrupt Handlers: Accessing Variables
- 6.32.2 Assembly Language Interrupt Handlers: Starting Condition
- 6.32.3 Assembly Language Interrupt Handlers: Preserve the Registers
- 6.33 Using Interrupt Eight in a TSR
- 6.34 Using int 8 Without Chaining
- 6.35 Using int 1C hex instead of int 8
- 6.36 Sample Program: Using int 1Ch With Critical Error and Ctrl-C Handling
- 6.37 Debugging Interrupt Handlers
- 7 HARDWARE INFORMATION AND PROGRAMMING
- 7.1 The 14.31818 MHz Clock
- 7.2 Clock Frequency Accuracy
- 7.3 The Counter/Timer Chip (CTC)
- 7.4 CTC Channels
- 7.4.1 CTC Channel Zero
- 7.4.2 CTC Channel Zero Default Operating Mode
- 7.4.3 CTC Channel One
- 7.4.4 CTC Channel Two
- 7.5 Speaker Interface
- 7.6 CTC Internal Registers
- 7.7 Access Modes
- 7.8 CTC Operating Modes
- 7.8.1 Operating Modes: Behaviour Common to All Modes
- 7.8.2 Operating Mode Zero: Interrupt on Terminal Count
- 7.8.3 Operating Mode One: Hardware-Retriggerable One-Shot
- 7.8.4 Operating Mode Two: Rate Generator
- 7.8.5 Operating Mode Three: Square Wave Generator
- 7.8.6 Operating Mode Four: Software-Triggered Strobe
- 7.8.7 Operating Mode Five: Hardware-Triggered Strobe
- 7.9 The 8254/8253 Registers
- 7.9.1 The Mode/Command Register
- 7.9.2 The Data Ports
- 7.9.3 Accessing the Registers
- 7.9.4 I/O Recovery Delays
- 7.10 Programming the Mode and Reload Register
- 7.11 Effect of Reprogramming Channel Zero on the Timer Tick Interrupt
- 7.12 Sample Program: Programming the Mode and Reload Value
- 7.13 Reading the Reload Register
- 7.14 Reading the Counting Register
- 7.15 The Latch Command
- 7.15.1 Meaning of Count Value in Mode Two
- 7.15.2 Meaning of Count Value in Mode Three
- 7.16 Sample Code: Reading the Count in Mode Two
- 7.17 The Lobyte/Hibyte Flag
- 7.18 The Read-back Command
- 7.19 Sample Code: Read-back
- 7.20 Reading the Count in Mode Three (8254 only)
- 7.21 Sample Code: Reading the Count in Mode Three
- 7.22 Sample Code: Optimised Mode Three Count Reading Function
- 7.23 Sample Program: Manipulate the CTC and Port B
- 7.24 Hardware Problems and Differences
- 7.24.1 Differences Between the Intel 8253 and 8254
- 7.24.2 Chipset Implementations
- 7.24.3 Intel 8253/8254/82C54 Clock Synchronisation Problems
- 7.25 Is the CTC an 8253 or an 8254?
- 7.26 Determining the Exact State of the CTC
- 7.27 Sample Program: Report Channel States
- 7.28 CTC Access under OS/2
- 7.28.1 OS/2 VTIMER.SYS: CTC Channel Zero
- 7.28.2 OS/2 VTIMER.SYS: CTC Channel One
- 7.28.3 OS/2 VTIMER.SYS: CTC Channel Two
- 7.29 Generating Audio Tones on the Speaker
- 7.30 Sample Program: Generating a Tone using CTC Channel Two
- 7.31 Timing Short Periods using CTC Channel Two
- 7.32 Timing Short Periods using Mode Three
- 7.33 Vertical Retrace
- 7.34 Sample Program: Timing Short Periods using Mode Three
- 7.35 The Real Time Clock (RTC)
- 7.35.1 Reading and Writing RTC Registers
- 7.35.2 Allocation of the RTC Registers
- 7.35.3 RTC Register A
- 7.35.4 RTC Register B
- 7.35.5 RTC Register C
- 7.35.6 RTC Register D
- 7.35.7 Reading the RTC
- 7.35.8 Sample Program: A TSR Clock using int 8 and the RTC
- 7.36 The RTC Interrupt and Related BIOS Functions
- 7.36.1 The BIOS Event Wait and Delay Functions
- 7.36.2 The BIOS RTC Interrupt Handler
- 7.36.3 Using the RTC Interrupt
- 7.36.4 Sample Program: Using the RTC Interrupt
- 7.37 Using CTC Channel One and Refresh Detect
- 7.37.1 Sample Program: Timing the Refresh Detect signal
- 7.37.2 Sample Code: delay(milliseconds) Function using Refresh Detect
- 8 SPEEDING UP THE TIMER TICK
- 8.1 The Fast Tick int 8 Handler
- 8.2 The Interface with the Mainline
- 8.3 Writing a Fast Tick Handler
- 8.4 Comments on Fast Timer Tick Interrupts
- 8.5 Sample Program: Morse Player using Fast Timer Tick
- 8.6 Dynamic Fast Tick Periods
- 8.7 Sample Program: Dynamic Fast Tick Interrupt Handler
- 9 READING AN ABSOLUTE TIMESTAMP
- 9.1 Sample Program: Absolute Time Reference (Timestamp) in Mode Two
- 9.2 Sample Program: Absolute Timestamp in Mode Two - Assembler
- 9.3 Handling the Midnight Boundary
- 10 OTHER TOPICS
- 10.1 The 586 Time Stamp Counter
- 10.2 Serial Port Regular Interrupt
- 10.2.1 Serial Port (UART) Documentation
- 10.2.2 Sample Program: Regular Interrupt using the Serial Port
- 10.2.3 Inserting Delays into Serial Port Transmitted Data
- 10.3 External Interrupt Sources
- 10.3.1 External Interrupt through Parallel Port
- 10.3.2 External Interrupt through Serial Port
- 10.3.3 External Interrupt through Sound Card
- 10.3.4 External Interrupt through Custom I/O Card
- 10.4 The Joystick Port
- 10.4.1 Joystick Port Hardware
- 10.4.2 Reading the Joystick Buttons and Position
- 10.4.3 Notes from the PC-GPE Article
- 10.4.4 Sample Program: Reading the Joystick Position
- 10.4.5 Using the Joystick Port for General Purpose Input
- 10.4.6 Joystick Left/Right and Up/Down Detection
- 10.5 The Mouse and Mouse Driver [not written]
- 10.6 Networks
- 10.7 Sound Generation
- 10.7.1 Pulse Width Modulation (PWM) Principle
- 10.7.2 PWM Audio Generation Implementation
- 10.7.3 Sample Program: DTMF Generation using PWM
- 10.7.3.1 Sample Program Explanation
- 10.7.3.2 Other Methods of Sound Generation
- 10.7.4 Peter Moylan's MUSIC Package
- 10.8 Related Software Packages
- 10.8.1 The ATIM Package
- 10.8.2 The MSCHRT and TCHRT Packages
- 10.8.3 The TCTIMER Package
- 10.8.4 The MILLISEC Package
- 10.8.5 The MSEC_12 Package
- 10.8.6 The ERTIMER Package
- 10.8.7 The FASTCLOK Package
- 10.9 Benchmarking Considerations
- 10.10 Granularity and Uncertainty
- 10.11 Converting between Microseconds and CTC Clocks
- 10.12 Maintaining a Millisecond or Microsecond Count
- 10.12.1 Sample Program: Millisecond Count using int 1Ch
- 10.13 Notes on Microsoft Windows
- 10.14 DOS File Date and Time Stamps
- 10.15 DOS and the Date and Time
- 10.15.1 DOS Date Rollover Bugs
- 10.16 Simulating a Vertical Retrace Interrupt
- 10.16.1 Vertical Retrace Interrupt Simulation Description
- 10.16.1.1 Measuring the Field Time
- 10.16.1.2 Controlling the CTC Interrupt
- 10.16.1.3 Significance of the SafeMargin Value
- 10.16.1.4 Overhead due to Large SafeMargin and Screen Update
- 10.16.1.5 Enhanced Handling of Missed Retrace Start
- 10.16.1.6 Other Notes
- 10.16.2 Sample Program: Simulating a Vertical Retrace Interrupt
- 10.16.3 Triple Buffering
- 11 QUESTIONS AND ANSWERS
- 11.1 Timing Accuracy
- 11.2 Timer Interrupts (int 8, int 1Ch, RTC Interrupt)
- 11.3 Interrupt Priorities and Nesting
- 11.4 Interrupt Handler Restrictions
- 11.5 High Speed Timer Tick
- 11.6 DOS Date and Time
- 11.7 Accessing Hardware
- 11.8 Miscellaneous
- 12 REFERENCES
-
- ## 1.3 AUTHOR AND DISTRIBUTION
-
- This document (including sample code and programs) is Copyright (c) 1994-1996
- by K. Heidenstrom. Please send corrections/additions/comments/suggestions to:
-
- Email: kheidens@actrix.gen.nz
- Snail mail: K. Heidenstrom, c/- P.O. Box 27-103, Wellington, New Zealand.
-
- If you send me comments, corrections etc via email or on a disk, you may find
- the quoter program described in section »» 1.8 helpful. It will generate a
- quoted copy of this file, to help you with marking up the document with your
- comments.
-
- The archive may be freely distributed via any electronic medium provided that
- it is not modified in any way, and that no charge (other than the normal charge
- to cover the disk, CD, etc) is made.
-
- The sample code and sample programs may be freely used in any commercial or
- non-commercial software.
-
- If you find this document useful, I would appreciate a postcard, or an email
- message, especially if you tell me a bit about your project.
-
- I'm pretty sure of this stuff, and I've done a bit of research (not as much as
- I should have done :-), but don't take it all as gospel. I have had to work
- some things out by myself and I may have got something wrong. If you know
- better about anything in here, please please drop me a message, so that other
- readers of this document can benefit from your experience. Thanks!
-
- FILE_ID.DIZ contents and SimTel information:
-
- pctim003.zip FAQ / App notes: Timing on the PC under DOS
-
- This archive contains a technical document useful to PC programmers,
- with many sample programs. The document covers timing and related
- subjects on the IBM PC family under DOS. Subjects include BIOS and
- DOS functions, the BIOS tick count, hardware interrupts, timer tick
- interrupts, Port B, the 8253/8254 timer, speeding up the timer tick,
- dynamic tick periods, simulated vertical retrace interrupt, double
- and triple buffering, absolute timestamping, the RTC, other timing
- methods, reading the joystick, PWM sound generation. Freeware.
- 13400 lines, PC ASCII, 340K ZIP file. Release 3, February 1996.
- Author: Kris Heidenstrom, kheidens@actrix.gen.nz.
-
- Simtel directory: SimTel/msdos/info/
-
- Keywords:
-
- 145818 8253 8254 8255 8259 AT B CTC BIOS Delay DOS I/O IBM Interrupt
- Joystick MS-DOS PC PIC PIT PWM Port RTC Tick Timestamping Timing
-
- This document should be named PCTIMxxx.TXT where xxx is the release number
- shown at the top of the file. The latest version will always be available
- on SimTel (ftp.coast.net), or mirrors (such as Oakland). The file's URL at
- SimTel is ftp://ftp.coast.net/SimTel/msdos/info/pctim*.zip.
-
- Your browser may not accept a wildcard specification (i.e. the asterisk), and
- may say that the file does not exist. If so, view a listing of the SimTel/
- msdos/info directory, find the file name, and modify the URL accordingly.
-
- ## 1.4 DISCLAIMER AND LEGAL STUFF
-
- I make no warranty of any kind with regard to this information and sample code.
- In no event shall I be liable for any damages whatsoever for any loss involving
- the use of this information or sample code, or due to any errors or omissions.
-
- Trademarks and service marks mentioned in this document are the property of
- their respective owners. Most of them probably know who they are :-)
-
- ## 1.5 DOCUMENT CONVENTIONS
-
- This file is formatted for viewing on an IBM or compatible (American ASCII
- with high-ASCII box characters, i.e. codepage 437) with an 80-column monospaced
- (i.e. text-mode) display, using tab stops every 8 columns. I have designed the
- document to work with DOS file viewers such as Vern Buerg's famous LIST program.
- Sections are hierarchically numbered. The contents is near the start of the
- file, and each section or subsection is announced by two '#' characters, a
- space, and the section number, to facilitate searching. I have mostly used
- British spelling.
-
- There are six illustrations in GIF format, which are enclosed in the FIGURES
- archive. Since they are line drawings, they do not look good if rescaled, so
- try to view them at their original resolutions if possible.
-
- Currently only the plain ASCII text version, in English, exists. There does
- not seem to be a good widely-used alternative at the moment. I would try Tex
- but I don't have a spare hard drive and six spare months to figure out how to
- use it! Let me know if you would like a Word Perfect 6.0 (DOS) or Word Perfect
- for Windows version and if there is enough interest I may create one. Also if
- you want to create an HTML version of this document, please get in touch!
-
- Numbers are decimal unless indicated. Hex is indicated by '0x' prefix or 'h'
- suffix, e.g. 0x55AA, 1Ch.
-
- Throughout this document, I refer to the 8253/8254 timer chip as the 'CTC'
- (counter/timer chip, or counter/timer circuit). This term is not normally used
- for this particular chip. Intel calls it the PIT (programmable interval timer).
- I mention this because you may get corrected if you publically call it the CTC.
-
- I have had a great deal of trouble maintaining a logical organisation in this
- document. I welcome any suggestions for improving its readability and
- understandability :-)
-
- Some subjects are outside my experience and I have marked these with (*).
- If you can fill in any of these gaps, this would be much appreciated.
-
- ## 1.6 SAMPLE CODE CONVENTIONS
-
- The sample code is in C and assembler, but you could convert it to Pascal or
- convert the C code to assembler. In most cases, I have aimed to be instructive
- rather than highly optimal. The sample programs are starting points - they are
- complete stand-alone programs, but are not necessarily very useful. They have
- been briefly tested with Borland C++ 2.0, Borland TASM 3.1, and Borland TLINK
- version 4.0. Short sample functions are untested. Let me know if you have any
- trouble with them.
-
- I have used small model for the C programs, so code and data are near, but this
- could be changed easily. The assembly language programs are in tiny model and
- should assemble with either MASM or TASM; I have had to forgo TASM's Ideal Mode
- and all of my nice macros. :-(
-
- I have listed #defines in each sample program as required. When I have re-used
- already-documented functions I have kept the name and coding the same, but have
- removed the comments from all but the first occurrence of the function.
-
- MS-DOS (version 2.0 or later) or a compatible operating system is assumed.
-
- ## 1.7 ACKNOWLEDGEMENTS
-
- My thanks for suggestions, information, criticism, and/or encouragement, to:
-
- Michael Bishop mxbish2@lookout.ecte.uswc.uswest.com
- Gordon Burditt gordon@sneaky.lonestar.org
- Jan-Pieter Cornet cornet@duteca2.et.tudelft.nl
- Saul Cozens s.cozens@sheffield.ac.uk
- David Empson dempson@actrix.gen.nz
- Klaus Hartnegg klaus@mailserv.brain.uni-freiburg.de
- Gian Uberto Lauri saint@dei.unipd.it
- William Luitje luitje@m-net.arbornet.org
- Terje Mathisen Terje.Mathisen@hda.hydro.com
- Michael Mauch mauch@uni-duisburg.de
- John Mertus mertus@brownvm.brown.edu {JAM}
- Peter Moylan peter@fourier.newcastle.edu.au
- Anders Roar Nielsen aroni@night.ping.dk
- Philip O'Carroll poc@maths.tcd.ie {POC}
- James Ralph jim@grc.com
- Paul Ross pa-ross@uwe.ac.uk
- Tor Sjowall tor@oslonett.no {TOR}
- Bob Smith bobs@access.digex.net
- John Stockton jrs@dclf.npl.co.uk
- Louis Warshaw louis@gate.net
-
- Please tell me if your name should be on this list!
-
- To give credit where it is due, throughout the text I have flagged specific
- contributions with the names shown in squiggly brackets.
-
- In particular, I have used (with permission) information about sampled audio
- generation on the PC speaker from a PC speaker music package written by Peter
- Moylan with help from Tim Channon. The technique mentioned here was also
- described by Mark Feldman (the PC-GPE guru). See section »» 10.7.
-
- I have also used many invaluable pieces of information (again with permission)
- from a collection of papers by Prof. John Mertus. Prof. Mertus's papers deal
- with subject testing (e.g. reaction timing), timer accuracy, and statistical
- analysis techniques for validating correct and reliable performance on various
- machines in various configurations (e.g. in protected mode, or on networked
- machines) which I have not covered in this document. They are thorough, and
- very interesting. You can FTP his files, in PostScript and LaTeX formats,
- from: ftp://jam.cog.brown.edu/pub/timing/ (various files).
-
- I have paraphrased his comments to maintain continuity in my document, and used
- the marker {JAM} so that credit goes where it is due. Any mistakes in the
- interpretation are mine, however. Prof. John Mertus owns the copyright on the
- above mentioned documents, please respect the considerable amount of work which
- has gone into them, by giving him credit if you use them.
-
- ## 1.8 QUOTER PROGRAM
-
- To generate a quoted version of this file so you can report problems to me, I
- have included in the SAMPLES archive a small program called QUOTE.COM, which
- operates as a quoting filter. Entering "C:\> QUOTE <PCTIMxxx.TXT >QUOTED.TXT"
- (where 'xxx' is the release number) will generate a quoted copy of this document
- for you to edit and mark up. You will probably want to use an editor that can
- handle more than 80 columns when editing the quoted copy.
-
- ## 1.9 REVISION NOTES
-
- Release 1 19950417
- Release 2 19950816
- Release 3 19960201
-
- This is the third release of this document. At this point, I have at last
- covered all the important timing-related subjects that I know about.
- If you would like to see any other subjects covered, or would like to submit
- documentation or code on other relevant subjects, please get in touch.
- Otherwise the only intended future changes will be for correctness and to
- resolve the items indicated with (*) if possible.
-
- Changes from release 2 to release 3:
-
- ■ Added information and sample program for vertical retrace interrupt simulation
- ■ Tidying up
- ■ Improved comparison of techniques
- ■ Various improvements suggested by Dr. John Stockton
- ■ Important note relating to long timer tick interrupt handlers added,
- see section »» 6.9.1.
- ■ Added questions and answers section
- ■ Added six illustrations in GIF format (hoping CI$ don't sue me :-)
- ■ Added discussion on int 8 versus int 1Ch
- ■ Added info on the triple buffering technique that can be used in
- conjunction with vertical retrace interrupt simulation
- ■ Brief mentions of Microchannel int 8 reset
- ■ Brief description of joystick left/right and up/down under interrupt
- ■ Several notes from Michael Mauch (mauch@uni-duisburg.de) included
- ■ Much expanded explanation of I/O access and recovery delays (section
- »» 7.9.4)
- ■ Version 1.1.0 of quoter program, proper tab handling
-
- Changes from release 1 to release 2:
-
- ■ Added sample program to read and write CTC registers and Port B with a
- command-oriented interface
- ■ Added information on timing-related software packages
- ■ Added brief notes on benchmarking considerations
- ■ Modified sample code for short period timing using channel 2 to generate a
- strobe pulse on the parallel port with a duration of 5 us plus overhead
- ■ Added code for converting between microseconds and CTC clocks
- ■ Corekkted twleve typoes
- ■ Fixed various minor clumsy explanations and stupid mistakes
- ■ Added notes on Windows considerations from {TOR}
- ■ Added description and sample function for handling midnight boundary
- when calculating elapsed time from absolute timestamp values
- ■ Added timing using Refresh Detect signal on Port B (thanks to William Luitje)
- ■ Added sample code to determine keyboard interface type (PC/XT or AT and later)
- ■ Documentation on resolution and uncertainty
- ■ Documentation and sample program for millisecond count variable
- ■ Added delay(milliseconds) function using Refresh Detect
- ■ Added Refresh Detect method of reading the joystick position
- ■ Added notes on generating delays in serially transmitted data
- ■ Added sample program to generate DTMF using PWM audio techniques
- ■ Added information on DOS internal handling of date and time
- ■ Include sample programs in executable form in the distribution file
-
- ## 1.10 GLOSSARY
-
- ASIC
- Application Specific Integrated Circuit, a high density custom chip.
- BCD
- Binary Coded Decimal, an encoding scheme where each digit of a decimal
- number is represented by four adjacent bits in a register. For example
- in BCD the number ninety-seven would be represented by 10010111 binary.
- The binary representation of 97 is 01100001.
- BIOS
- Basic Input/Output System, software in ROM chips on the motherboard.
- Bit
- If you don't know what a bit is, you are reading the wrong document :-)
- Channel
- One of three independent counting or timing circuits in the CTC. Also
- referred to as a 'timer'.
- Clock
- [n] An electrical signal at a fixed frequency [in this context].
- [v] To trigger to perform a certain action. For an electrical clock,
- the action is performed at the instant of the rising or falling
- edge of the clock signal.
- Count
- [n] The value in a counter at a given moment in time.
- [v] What it usually means :-)
- Counter
- A register which increments or decrements when clocked.
- Counting register
- The counter in a CTC channel. It decrements when clocked, and can be
- reloaded from the Reload register. See section »» 7.3
- CTC
- Counter/Timer Chip (or Circuit), the 8253 (PC, XT) or 8254 (AT and
- later) chip or functional equivalent. I prefer the term 'CTC' and
- use it in this document, but the CTC is more commonly known as the
- 'Timer', the 'Counter', and the 'PIT' (Programmable Interval Timer),
- which is Intel's name for the chip.
- CTC clock
- The clock input frequency to the CTC, 1.193181666666... MHz.
- Decrement
- Count down (usually by 1).
- Divide [frequency]
- To generate a lower frequency from a higher frequency by counting
- pulses and producing an output pulse when a certain number of input
- pulses have occurred.
- Divisor register
- Another name for the Reload register when modes 2 or 3 are used.
- See section »» 7.3.
- DMA
- Direct Memory Access, a technique where hardware (e.g. a floppy disk
- drive adapter or sound card) transfers data directly to or from memory,
- without processor intervention.
- EISA
- Enhanced Industry Standard Architecture, the bus structure used in some
- more modern PCs. It is an extension of the ISA architecture.
- EOI
- End of Interrupt, a command to the PIC to indicate that an interrupt
- handler has completed, see section »» 6.28.
- Flag
- A single bit indicating yes/no, true/false, on/off, enabled/disabled,
- or any condition which has two possible (and usually opposite) states.
- Frequency
- How often something occurs, per second. 18.2065 Hz (hertz) means
- 18.2065 times per second.
- Hz
- Hertz, the unit of frequency.
- IMR
- Interrupt Mask Register in the PIC.
- Increment
- Count up (usually by 1).
- Interrupt
- [n] A hardware- or software-generated interruption to the processor.
- [v] To suspend processing and cause the processor to execute a special
- section of code (the interrupt handler).
- Interrupt Controller
- See PIC.
- Interrupt Handler
- See Interrupt Service Routine.
- Interrupt Service Routine
- A section of code which is executed in response to an interrupt which
- 'services' (attends to) the hardware device or software invocation
- which generated the interrupt.
- Interrupt Vector
- See Vector.
- IRQ
- Interrupt request, a hardware interrupt source, handled by the PIC(s).
- IRR
- Interrupt Request Register, part of the 8259 PIC, see section »» 6.12.
- ISA
- Industry Standard Architecture (Also Irritatingly Slow Architecture),
- the bus structure of the PC, XT, and AT. Contrast to EISA, MCA and PCI
- architectures. Despite its limitations, it is still the most common bus
- structure. Many of these limitations are avoided with the VESA Local
- Bus extension.
- ISR
- Interrupt Service Routine. Also In Service Register, section »» 6.13.
- IVT
- Interrupt Vector Table, a table of 256 interrupt vectors occupying the
- first 1024 bytes of physical memory (in real and 8086 emulation modes).
- {JAM}
- See section »» 1.7.
- Jitter
- Unevenness, inconsistency, fluctuation, variation, or irregularity.
- LSI
- Large Scale Integration, a high density chip, see ASIC
- MCA
- Microchannel Architecture, the bus structure used in most IBM PS/2
- machines. Sort of a dead duck as far as architectures are concerned.
- MHz
- Megahertz, one million hertz.
- Mode
- Of a CTC channel, the operational algorithm, or definition of behaviour,
- which has been selected (programmed) for that channel.
- Monostable
- A circuit which has one stable state (in which it will remain until
- triggered externally) and one unstable state (in which it will remain
- for a given period of time). Also called a one-shot. When triggered,
- it switches to its unstable state, and after a period of time, it
- returns to its stable state until triggered again.
- ms
- Millisecond(s), one thousandth of a second.
- NMI
- Non-Maskable Interrupt, an emergency interrupt source that cannot be
- masked (cannot be disabled under software control).
- PIC
- Programmable Interrupt Controller, an Intel 8259 chip or functional
- equivalent, which arbitrates IRQs and issues hardware interrupt
- requests to the processor. The PC and XT have one PIC, the AT has two.
- See section »» 6.4.
- {POC}
- See section »» 1.7.
- Port
- A link between software and hardware. Allows software to 'talk' to
- hardware devices. Also a connector on the back of the PC (e.g. serial
- or parallel port).
- POST
- Power-On Self-Test, the initialisation and test functions of the BIOS.
- PPI
- Programmable Peripheral Interface, an Intel 8255, used on the PC and XT,
- replaced by the keyboard controller on the AT and later machines.
- ppm
- Parts Per Million. 10000 ppm is one percent. 1 ppm is 0.0001 percent.
- 1 ppm corresponds to 0.0864 seconds per day; 11.5741 ppm is one second
- per day.
- Prefetch queue
- A look-ahead buffer in the processor which 'pre-fetches' instructions
- ahead of the current execution point during gaps when memory is not
- being accessed (i.e. while instructions are being internally processed
- by the processor) so that the instructions are ready before they are
- needed. This method is based on the assumption that instructions are
- executed in sequence. A jump, call, return, interrupt, or conditional
- branch instruction (if the branch is taken) disrupt this sequence and
- cause the prefetch queue to be flushed, slowing execution.
- Processor
- The Intel 80x86 central processing unit or functional equivalent.
- Reload register
- Register which contains the value which is reloaded into the Counting
- register under certain circumstances (depending on the mode), see
- section »» 7.3.
- Register
- A group of bits, can be used to store and manipulate numbers.
- ROM
- Read-Only Memory, a chip containing factory programmed software.
- RTC
- Real Time Clock, also called RTC/RAM or CMOS. A Motorola MC146818
- or workalike, containing real-time date and time registers and
- battery-backed-up storage for BIOS parameters (CMOS).
- Tick
- The timer interrupt which normally occurs 18.2065 times per second.
- Timer
- See 'Channel' and 'CTC'.
- TLA
- Itself
- {TOR}
- See section »» 1.7.
- TSR
- Terminate and Stay Resident, a memory-resident pop-up utility program.
- UART
- Universal Asynchronous Receiver/Transmitter; a chip which transmits and
- receives asynchronous serial data (e.g. to a modem). The UART used in
- the PC is the 8250 or one of its descendants.
- us
- Microsecond(s), one millionth of a second.
- Vector
- [n] A pointer to a section of code, often an interrupt service routine.
- [v] To execute the code pointed to by a vector.
- VGA
- A video adapter standard. It is the basic standard for most current
- video hardware. The name comes from Video Graphics Array, the ASIC
- that implements the video hardware in the PS/2.
- -WR
- An active low write signal. The '-' prefix means active low. When
- this line goes low, the processor is writing data into a peripheral.
-
- ## 2 OVERVIEW OF TIMING TECHNIQUES
-
- This section gives you the big picture, then presents the timing techniques
- that will be described in detail in later sections, so you can choose the
- technique that interests you.
-
- ## 2.1 THE BIG PICTURE
-
- Figure 1 (in the accompanying FIGURES archive) gives a general overview of the
- two main timing subsystems in the PC, and their interfaces to the processor.
-
- The 14.31818 MHz system clock is divided by 12 to give a 1.193182 MHz clock
- (period is 0.8381 microseconds) which clocks the three channels of the 8253/8254
- counter/timer chip (CTC). The CTC divides this frequency to lower frequencies
- using programmable divisors, and produces three output signals.
-
- CTC channel zero's output is connected directly to IRQ0 on the primary PIC (8259
- interrupt controller chip), and generates int 8, the timer tick interrupt, about
- 18.2065 times per second, or once every 54.9254 milliseconds. The timer tick is
- a regular interrupt which allows certain actions (such as updating the system
- time-of-day) to be executed periodically.
-
- Interrupt 8 is serviced by the ROM-BIOS. The BIOS's int 8 handler increments
- the BIOS tick count variable (a 32-bit variable used for timekeeping) and turns
- off the floppy disk drive motors two seconds after they were last accessed. It
- also issues int 1C hex, which may be used as a regular interrupt source by user
- programs.
-
- The BIOS tick count is a 32-bit counter at low memory address 0040:006C, which
- contains the number of timer ticks (units of 54.9254 ms) since midnight and is
- used by DOS to calculate the time of day.
-
- CTC channels 1 and 2 can also be used for timing, via the Refresh Detect and
- Timer 2 readback signals on Port B. Channel 2 also generates audio for the
- PC speaker, and can be used in conjunction with channel 0 for PWM audio
- generation.
-
- The CTC divides its 1.193182 MHz clock down to 18.2065 Hz using a 16-bit
- counter. It is possible to read the actual count in progress in the CTC.
- In combination with the tick count variable, this can give an absolute time
- value, in units of 0.8381 us, for timestamping, elapsed time calculation, etc.
-
- In some applications, a timer tick rate faster than 18.2065 times per second is
- required. This can be achieved by reprogramming the CTC. The CTC is told to
- generate the timer tick at a faster rate, and the program intercepts the timer
- tick interrupt (int 8). The int 8 handler does its thing, and calls the old
- int 8 handler at the correct rate (18.2065 times per second) to maintain the
- correct system time.
-
- The Real Time Clock (RTC) was introduced with the AT, and all hardware-
- compatible ATs and later machines have one. The RTC is completely independent
- of the CTC. It uses a 32.768 kHz watch crystal for timekeeping and is battery
- backed up (i.e. continues to keep time while the computer is powered off).
- It can be used to generate a periodic interrupt, usually at 1024 Hz (1024
- interrupts per second).
-
- ## 2.2 WHICH TECHNIQUE?
-
- There are three basic approaches to timing. Often two approaches can be used
- together. The techniques are summarised and compared in section »» 2.3.
-
- ■ ABSOLUTE TIME REFERENCE
- You can write a function for use by your program that returns a value
- representing the absolute time, with units and resolution of one tick
- (54.9254 ms), or 977 us (the RTC regular interrupt rate), or one CTC
- clock (0.8381 us).
-
- ■ RELATIVE TIME REFERENCE
- Your program can use the CTC to measure short time durations, for
- example to generate a short pulse on an I/O port pin or measure an
- external signal.
-
- ■ REGULAR INTERRUPT
- An interrupt handler is called at regular (or sometimes, irregular)
- intervals, e.g. the default rate of once every 54.9254 ms, or 1024
- times per second using the RTC, or at a user-selectable rate if you
- reprogram the CTC. The interrupt handler can perform operations in
- the background and/or maintain an absolute time variable.
-
- ## 2.3 COMPARISON OF TECHNIQUES
-
- 'Special precautions' in the following table refers to intercepting the DOS
- Ctrl-C, Critical Error, and Divide Overflow vectors so that interrupt vectors
- and/or hardware states can be restored safely when the program is terminated
- (see section »» 5 and subsections).
-
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Call DOS to read time-of-day │
- │ Type: Absolute time reference │
- │ Resolution: 55 ms or one second │
- │ Special precautions: Not required │
- │ Use in TSRs: Not without special TSR techniques │
- │ Works under OS/2: Yes │
- │ Notes: Portable to all DOS and DOS compatible systems │
- │ Applications: Low resolution, absolute time value │
- │ Described in: Section »» 3.1 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Call BIOS RTC functions to read time-of-day │
- │ Type: Absolute time reference │
- │ Resolution: One second │
- │ Special precautions: Not required │
- │ Use in TSRs: Usually safe │
- │ Works under OS/2: Yes │
- │ Applications: Low resolution, absolute time value │
- │ Described in: Section »» 3.2 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Read RTC time of day directly │
- │ Type: Absolute time reference │
- │ Resolution: One second │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Probably │
- │ Applications: Low resolution, absolute time value │
- │ Described in: Section »» 7.35 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Use the BIOS tick count variable │
- │ Type: Absolute time reference │
- │ Resolution: 55 ms │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Yes │
- │ Notes: Can be read from within an interrupt routine │
- │ Applications: General absolute time value, low resolution │
- │ Described in: Section »» 4 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Use int 1C hex │
- │ Type: Regular interrupt │
- │ Resolution: 55 ms │
- │ Special precautions: Required │
- │ Use in TSRs: No (see section »» 6.35) │
- │ Works under OS/2: Yes │
- │ Applications: Low resolution regular interrupt │
- │ Described in: Section »» 6.1, section »» 6.35 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Intercept int 8 (in TSRs) │
- │ Type: Regular interrupt │
- │ Resolution: 55 ms │
- │ Special precautions: Not required if used in a TSR │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Yes │
- │ Applications: Regular interrupt for timing and/or popup by TSRs │
- │ Described in: Section »» 6.33 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Read CTC channel 0 on-the-fly in mode two │
- │ Type: Absolute timestamp │
- │ Resolution: 0.8381 us │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Only if HW_TIMER = ON │
- │ Notes: Can be read from within an interrupt routine │
- │ Applications: Absolute time value, high resolution │
- │ Described in: Section »» 7.16 and section »» 9 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Read CTC channel 0 on-the-fly in mode three │
- │ Type: Absolute timestamp │
- │ Resolution: 0.8381 us │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Only if HW_TIMER = ON │
- │ Notes: Can be read from within an interrupt routine │
- │ Will not work on a PC, XT, or PS/2 │
- │ No advantages over using mode two │
- │ Applications: Absolute time value, high resolution │
- │ Described in: Section »» 7.20, section »» 7.21, section »» 7.22 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Use CTC channel 2 for timing short delays │
- │ Type: Relative time reference │
- │ Resolution: 0.8381 us │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Only if HW_TIMER = ON │
- │ Notes: Can be used within an interrupt routine │
- │ Good for implementing short timeouts │
- │ Should only be used with interrupts locked out │
- │ Disrupts the system beep if used under interrupt │
- │ Applications: Short delays, useful in dedicated hardware control │
- │ Described in: Section »» 7.31, section »» 10.4.4 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Read CTC channel 0 in mode three for short delays │
- │ Type: Relative time reference │
- │ Resolution: 0.8381 us │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Only if HW_TIMER = ON │
- │ Notes: No advantages over using mode two │
- │ Applications: Short delays, useful in dedicated hardware control │
- │ Described in: Section »» 7.32 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Vertical Retrace (polled) │
- │ Type: Relative time reference │
- │ Resolution: Medium (1/60 or 1/72 of a second) │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Probably not │
- │ Notes: Useful for synchronising to screen scan │
- │ Applications: Screen scan synchronisation in games, graphics apps │
- │ Described in: Section »» 7.33 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: RTC Periodic Interrupt │
- │ Type: Regular interrupt │
- │ Resolution: 976.5625 us │
- │ Special precautions: Required │
- │ Use in TSRs: Not really safe │
- │ Works under OS/2: Probably not │
- │ Notes: Doesn't interfere with the CTC │
- │ Convenient resolution │
- │ Won't work on PCs and XTs │
- │ Applications: Programs that slow the machine or time other programs │
- │ Described in: Section »» 7.36 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: BIOS Delay and Event Wait functions │
- │ Type: Relative delay │
- │ Resolution: 976.5625 us │
- │ Special precautions: May be required │
- │ Use in TSRs: Not safe │
- │ Works under OS/2: Probably not │
- │ Notes: Doesn't interfere with the CTC │
- │ Won't work on PCs and XTs │
- │ Applications: General delays or timeouts with about 1ms resolution │
- │ Described in: Section »» 7.36.1 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Refresh Detect (CTC channel 1 read-back) │
- │ Type: Relative time reference │
- │ Resolution: 15.0857 us │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: No │
- │ Notes: High resolution │
- │ Very tidy way to generate short delays │
- │ Can be used to generate delays of 'at least x' with │
- │ interrupts enabled │
- │ Can be used within an interrupt routine │
- │ Interrupts, if enabled, will lengthen the delay │
- │ Won't work if the RAM refresh rate has been changed │
- │ Won't work on old PCs and XTs │
- │ Applications: Short delays, timeouts, timing input signals │
- │ Described in: Section »» 7.37 │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Speed up CTC channel 0 (timer tick) rate │
- │ Type: Regular or irregular interrupt │
- │ Resolution: Settable │
- │ Special precautions: Required │
- │ Use in TSRs: No │
- │ Works under OS/2: Only if HW_TIMER = ON │
- │ Notes: Can generate exact interrupt rate (e.g. 500us, 1ms) │
- │ May affect other DOS sessions under OS/2 with HW_TIMER │
- │ Applications: Fast regular interrupt source - used for games, etc │
- │ Described in: Section »» 8 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Intel 586 Time Stamp Counter │
- │ Type: Absolute or relative time reference │
- │ Resolution: Extremely high │
- │ Special precautions: Not required │
- │ Use in TSRs: Yes │
- │ Works under OS/2: Probably │
- │ Notes: Ridiculously high resolution │
- │ Disadvantages: Doesn't work on 486 or lower │
- │ Not guaranteed to work on future processors │
- │ Timing unit depends on processor clock speed │
- │ Applications: High resolution timestamping for usage billing │
- │ Described in: Section »» 10.1 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: Regular interrupt from serial port │
- │ Type: Regular interrupt │
- │ Resolution: Selectable │
- │ Special precautions: Required │
- │ Use in TSRs: Not reliably │
- │ Works under OS/2: No │
- │ Notes: User-selectable interrupt rate │
- │ Doesn't affect the CTC or the RTC │
- │ Requires a spare serial port │
- │ Applications: Slow or fast regular interrupt │
- │ Described in: Section »» 10.2 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
- ┌──────────────────────────────────────────────────────────────────────────────┐
- │ Technique: External regular or irregular interrupt source │
- │ Type: Regular or irregular interrupt │
- │ Resolution: Depends on external hardware │
- │ Special precautions: Required │
- │ Use in TSRs: May not be reliable │
- │ Works under OS/2: Probably not │
- │ Notes: Can be very versatile │
- │ Requires special hardware │
- │ Applications: Slow or fast interrupt using special hardware │
- │ Described in: Section »» 10.3 and subsections │
- └──────────────────────────────────────────────────────────────────────────────┘
-
- ## 2.4 OTHER SUBJECTS COVERED IN THIS DOCUMENT
-
- I've included, in addition to timing related documentation, info on handling
- the DOS Ctrl-C, critical error, and divide overflow interrupts (required if
- you are going to intercept any other interrupts), see section »» 5 and
- subsections, lots of general information about interrupts, information on
- various relevant hardware devices, information on the joystick hardware,
- information on sound and music generation using a technique called PWM (see
- section »» 10.7), and information on vertical retrace interrupt emulation
- (section »» 10.16).
-
- ## 3 DOS AND BIOS TIME-OF-DAY AND ALARM FUNCTIONS
-
- In high level languages, library functions are available to get the time of day
- and should be used for portability. Internally they use the DOS time of day
- functions. Assembly language programmers can use the DOS and BIOS functions
- directly.
-
- ## 3.1 READING THE DATE AND TIME FROM DOS
-
- DOS functions 2A, 2B, 2C, and 2D hex relate to time of day. To use them, set
- AH to the function number, and set other registers as applicable, and issue int
- 21 hex. All values are accepted and returned in binary form (i.e. not BCD).
-
- Get Date : DOS functions (int 21h)
- Call with: AH = 2A hex
- Returns: AL = Day of week (0 to 6 correspond to Sun to Sat)
- CX = Year in full (1980 to 2099, 7BCh to 833h)
- DL = Day of month (1 to 31)
- DH = Month of year (1 to 12 correspond to Jan to Dec)
-
- Get Time : DOS functions (int 21h)
- Call with: AH = 2C hex
- Returns: CH = Hours (0 to 23, using 24-hour clock format)
- CL = Minutes (0 to 59)
- DH = Seconds (0 to 59)
- DL = Hundredths of seconds (0 to 99) (see note below)
-
- Set Date : DOS functions (int 21h)
- Call with: AH = 2B hex
- CX = Year in full (must be 1980 to 2099)
- DL = Day of month (1 to 31, depending on month)
- DH = Month of year (1 to 12)
- Returns: AL = Success/failure: 0 = OK, 0FFh = Bad date specified
-
- Set Time : DOS functions (int 21h)
- Call with: AH = 2D hex
- CH = Hours (0 to 23, 24-hour clock format)
- CL = Minutes (0 to 59)
- DH = Seconds (0 to 59)
- DL = Hundredths of seconds (0 to 99) (see note below)
- Returns: AL = Success/failure: 0 = OK, 0FFh = Bad time specified
-
- The time of day is calculated from the BIOS tick count variable. The hundredths
- of seconds value is approximated using an internal algorithm which apparently
- produces an even distribution of values, but its resolution is only as good as
- the tick counter, i.e. 54.9254 ms. See section »» 10.15 for more information.
-
- ## 3.2 READING THE DATE AND TIME FROM THE BIOS
-
- BIOS functions provide access to the tick count and the RTC (Real-Time Clock),
- accessed by issuing int 1A hex. (The BIOS tick count functions are also part
- of this interrupt, but should not be used - see section »» 4.3 for details).
- The RTC functions accept and return values in BCD form.
-
- The RTC functions are present on the AT and all later machines, but not on
- the original PC or XT (there may be some hybrid machines that do support them,
- but I don't know of any).
-
- Get RTC Date : int 1Ah
- Call with: AH = 04 hex
- Returns: CH = Hundreds of years (19h or 20h, BCD format)
- CL = Year (00h to 99h, BCD format)
- DH = Month (01h to 12h, BCD format)
- DL = Day of month (01h to 31h, BCD format)
- CF = Error status, carry is set if clock is not running
-
- Get RTC Time : int 1Ah
- Call with: AH = 02 hex
- Returns: CH = Hours (00h to 23h, BCD format)
- CL = Minutes (00h to 59h, BCD format)
- DH = Seconds (00h to 59h, BCD format)
- CF = Error status, carry is set if clock is not running
-
- Set RTC Date : int 1Ah
- Call with: AH = 05 hex
- CH = Hundreds of years (19h or 20h, BCD format)
- CL = Year (00h to 99h, BCD format)
- DH = Month (01h to 12h, BCD format)
- DL = Day of month (01h to 31h, BCD format)
- Returns: Nothing
-
- Set RTC Time : int 1Ah
- Call with: AH = 03 hex
- CH = Hours (00h to 23h, BCD format)
- CL = Minutes (00h to 59h, BCD format)
- DH = Seconds (00h to 59h, BCD format)
- DL = Daylight saving flag:
- 00 = Standard time
- 01 = Daylight saving time
- Returns: Nothing
-
- ## 3.3 SAMPLE PROGRAM: DOS DEVICE DRIVER FOR THE AT CLOCK
-
- The following program implements an installable DOS device driver for the AT
- clock, using the BIOS RTC functions. Save the following code section as
- ATRTC.ASM and assemble according to the instructions in the comment block.
-
- -------------------------------- snip snip snip --------------------------------
- NAME ATRTC
-
- ; Sample program #1
- ; DOS Device Driver for the AT Real Time Clock
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This program assembles into ATRTC.SYS, an installable DOS device driver that
- ; removes DOS's dependence on the BIOS timer tick count variable, using the AT
- ; BIOS's Real Time Clock functions to get and set the current date and time.
- ; This program does not support the daylight saving feature of the RTC.
- ; At installation, it checks that the machine is an AT, and that the RTC is
- ; functional. If either check fails, it installs but remains inactive.
- ;
- ; Save this file to ATRTC.ASM and assemble with:
- ; masm atrtc;
- ; link atrtc;
- ; exe2bin atrtc.exe atrtc.sys
- ; or
- ; tasm atrtc;
- ; tlink atrtc;
- ; exe2bin atrtc.exe atrtc.sys
- ;
- ; Then place ATRTC.SYS in your root directory, DOS directory, or utilities
- ; directory, and add the line DEVICE=<path>\ATRTC.SYS to your CONFIG.SYS
- ; file, where <path> specifies the directory path to ATRTC.SYS. If you want
- ; to load ATRTC.SYS high, use DEVICEHIGH= or HIDEVICE= instead of DEVICE= to
- ; load the driver.
-
- BinFile SEGMENT
- ASSUME cs:BinFile,ds:nothing,es:nothing,ss:nothing
-
- ORG 0
- Origin:
-
- ; Device driver header
-
- Header DD -1 ; Link to next device
- Attrib DW 8008h ; Attribute word
- DW Strategy ; Strategy entry point
- DW Interrupt ; Interrupt entry point
- DB "CLOCK$ " ; Device name
-
- ; When a request is made for this device, DOS calls the "Strategy" routine,
- ; passing a pointer to the request header in ES:BX. The strategy routine saves
- ; this pointer in ReqHdr and returns to DOS. DOS then calls the "Interrupt"
- ; routine, which executes the request specified by the request header.
-
- ReqHdr DD 0 ; Far pointer to request header
-
- InitPtr DW Init ; Address of init function
-
- MonthTbl1 DW 0,31,59,90,120,151,181,212,243,273,304,334,365 ; Normal
- MonthTbl2 DW 0,31,60,91,121,152,182,213,244,274,305,335,366 ; Leap yr
-
- Strategy PROC far ; Save address of Request Header
- mov WORD PTR ReqHdr+0,bx
- mov WORD PTR ReqHdr+2,es
- retf ; Back to DOS
- Strategy ENDP
-
- Interrupt PROC far
- push ds
- push si
- push dx
- push cx
- push bx
- push ax ; Preserve registers
- lds bx,ReqHdr ; Point DS:BX to Request Header
- mov WORD PTR ds:[bx+3],100h ; No errors, completed
- mov al,ds:[bx+2] ; Get command number from Request Header
- mov cx,OFFSET Read ; Prepare for Read command
- cmp al,4 ; Check for Read command
- je GotAdr ; If so
- mov cx,OFFSET Write ; Prepare for Write command
- cmp al,8 ; Check for Write command
- je GotAdr ; If so
- cmp al,9 ; Check for Write with Verify
- je GotAdr ; If so
- mov cx,InitPtr ; Prepare for Init command
- cmp al,0 ; Check for init command
- je GotAdr ; If so
- mov cx,OFFSET Null ; If none of above, use Null routine
- GotAdr: call cx ; Dispatch to appropriate handler
- pop ax
- pop bx
- pop cx
- pop dx
- pop si
- pop ds ; Restore all regs
- retf
- Interrupt ENDP
-
- ; These command code subroutines called by "Interrupt" Routine. They are called
- ; with DS:BX pointing to the request header. They do not return an error code.
-
- Read PROC near ; Function 4 = Read
- lds bx,ds:[bx+14] ; Point DS:BX to buffer area
- push bx ; Keep offset
-
- ; Get date, check clock is working
-
- mov ah,4
- int 1Ah ; Read RTC date
- jnc NoRTCErr1 ; If alright, continue
- xor cx,cx ; Assume 1980
- jmp SHORT StoreYear ; Don't do calculations
-
- ; Calculate year (1980 - 2099) in binary form
- ; Note - the above check for a date less than 1980 was suggested by Michael
- ; Mauch (mauch@uni-duisburg.de). He reports that his BIOS (AMI, 06/06/92)
- ; has a bug which causes years 20xx to be reported as 19xx. The following
- ; workaround handles this bug.
-
- NoRTCErr1: cmp cx,1980h ; Check for BIOS returning year
- jae YearValid ; 19xx when it should be 20xx
- mov ch,20h ; If so, fix it
- YearValid: mov al,cl ; Get years (00-99)
- call BCDToBinary ; Convert to binary
- cbw ; Zero AH
- push ax ; Keep it
- mov al,ch ; Get hundreds of years
- call BCDToBinary ; Convert to binary
- mov ah,100 ; Factor
- mul ah ; Get centuries x 100
- pop cx ; Restore year 0-99
- add ax,cx ; Now have absolute year in AX.
-
- xor cx,cx ; Zero day counter
- mov bx,1980 ; Starting year
-
- ; Year calculation stuff - AX is current year (1980 to 2099) read from RTC,
- ; BX is year being evaluated, CX is count of days so far. SI points to the
- ; appropriate month table for this year.
- ; Leap year algorithm: If the year is a multiple of four, it is a leap year,
- ; unless it's also a century, in which case it is not a leap year, except
- ; centuries that are a multiple of 400 years (e.g. 2000), in which case it
- ; is a leap year. In this case, the only century involved is 2000, thus just
- ; checking for a multiple of four is enough. If it's a multiple of four, it
- ; is a leap year, i.e. 366 days instead of 365.
- ;
- ; Note - There is a way to do this without looping and accumulating, using a
- ; clever little formula, but I will use this method, because I don't want to
- ; waste the time I spent getting this method to work :-)
-
- FindYearLp: mov si,OFFSET MonthTbl1 ; Prepare for not leap year
- test bl,3 ; Leap year?
- jnz NotLeap1 ; If not
- mov si,OFFSET MonthTbl2 ; If leap year, use leap year table
- NotLeap1: cmp bx,ax ; Got to this year yet?
- jae GotYear1 ; If so
- add cx,cs:[si+24] ; Add number of days in this year
- inc bx ; Increment year number
- jmp SHORT FindYearLp ; Loop to find year
-
- ; Now have BX containing number of days since 1st of January 1980 for the start
- ; of the current year - now incorporate the month and the day-of-month.
-
- GotYear1: mov al,dh ; Get month, 1-12, BCD
- call BCDToBinary ; Convert to binary
- cbw ; Zero AH
- shl ax,1 ; Double for word sized table
- mov bx,ax ; Month (1-12) to BX
- add cx,cs:[si+bx-2] ; Get month start, adjusted for 1-12
-
- mov al,dl ; Get day of month in BCD, 1-31
- call BCDToBinary ; Convert to binary
- dec ax ; Convert to zero-up
- cbw ; Zero hibyte
- add cx,ax ; Add in too.
-
- StoreYear: pop bx ; Restore offset of data structure
- mov ds:[bx+0],cx ; Store days since 1980 in structure
-
- mov ah,2
- int 1Ah ; Read RTC time
-
- jnc NoRTCErr2 ; If alright
- xor cx,cx ; If bad, zero values
- xor dx,dx
-
- NoRTCErr2: mov al,ch ; Hours
- call BCDToBinary ; To binary
- mov ds:[bx+3],al ; Store in DOS's data structure
- mov al,cl ; Minutes
- call BCDToBinary ; To binary
- mov ds:[bx+2],al ; Store
- mov al,dh ; Seconds
- call BCDToBinary ; To binary
- mov ds:[bx+5],al ; Store seconds
- mov BYTE PTR ds:[bx+4],0 ; Hundredths of seconds are zero
- Null: ret ; Return to handler dispatcher
- Read ENDP
-
- BCDToBinary PROC near ; Convert AL BCD to binary
- push cx
- mov ch,al ; Copy value to CH
- mov cl,4
- shr al,cl ; Shift top nibble down
- mov cl,10
- mul cl ; Get ten times the high digit
- and ch,0Fh ; Low digit only in CH
- add al,ch ; Add low digit
- pop cx
- ret ; Destroys AX and flags only
- BCDToBinary ENDP
-
- Write PROC near ; Functions 8 and 9 = Write
- lds bx,ds:[bx+14] ; Point DS:BX to buffer area
- push bx ; Keep for later
- mov dx,ds:[bx+0] ; Get number of days since 1980
-
- ; Determine the year, by successively accumulating days starting at 1980 until
- ; we exceed the number of days since 1980 that was provided by DOS. Once we
- ; pass the right year, adjust the number of days back again. We then have the
- ; year and the number of days within that year.
-
- mov ax,1980 ; Start at year 1980
- xor cx,cx ; Clear day accumulator
- DayAddLp2: mov bx,365 ; Assume for 365 days this year
- test al,3 ; Is current year a leap year?
- jnz NotLeap2 ; If not, keep the 365
- inc bx ; If so, use 366
- NotLeap2: add cx,bx ; Add number of days in this year
- cmp cx,dx ; Have we gone past the year we want?
- ja GotYear2 ; If so, have current year in BX
- inc ax ; If not, increment the year
- jmp SHORT DayAddLp2 ; Loop
- GotYear2: sub cx,bx ; Get number of days up to start of year
- sub dx,cx ; Get remainder (Months and Days)
-
- mov si,OFFSET MonthTbl1 ; Prepare for not leap year
- test al,3 ; Leap year?
- jnz NotLeap3 ; If not
- mov si,OFFSET MonthTbl2 ; If leap year, use leap year table
-
- ; Here, AX contains the absolute year in binary, DX contains the number of
- ; days offset into that year, in the range 0 - 364 (or 0 - 365 for leap years)
- ; and SI points to the appropriate month table for the year being set.
-
- NotLeap3: mov bl,100 ; Divisor
- div bl ; Get AL = century (19 or 20), AH = year
- mov bx,ax
- call BinaryToBCD ; Convert century to BCD
- xchg al,bh ; To BH, get year within century
- call BinaryToBCD ; To BCD
- xchg al,bl ; To BL, and get year in binary to AL
- push bx ; Keep value for CX for Set RTC Date
-
- ; Now calculate month and day of month from number of days offset into year (DX)
-
- xor bx,bx ; Point to start of table
- CompareMonth: inc bx
- inc bx ; Move to next month entry
- cmp dx,cs:[si+bx] ; Compare to start of next month
- jae CompareMonth ; If DX is not less than table entry
- sub dx,cs:[si+bx-2] ; Subtract number of days in months
-
- ; Now have DL = day of month (zero-up), and BL = month of year (1-12) x 2.
-
- xchg ax,dx ; Get day of month (0-30) to AL
- inc ax ; Convert to 1-31
- call BinaryToBCD ; Convert to BCD
- xchg ax,dx ; To DL
- xchg ax,bx ; Get month x 2 from BL
- shr al,1 ; Get month number, 1-12
- call BinaryToBCD ; Convert to BCD
- mov dh,al ; To DH
- pop cx ; Restore years and hundreds of years
-
- mov ah,5
- int 1Ah ; Set RTC date
-
- ; Now set the time
-
- pop bx ; Restore pointer to DOS's data buffer
- mov al,ds:[bx+5] ; Read seconds from DOS
- call BinaryToBCD ; Convert to BCD
- mov dh,al ; To DH
- xor dl,dl ; No daylight saving flag
- mov al,ds:[bx+3] ; Read hours
- call BinaryToBCD ; Convert to BCD
- mov ch,al ; To CH
- mov al,ds:[bx+2] ; Read minutes
- call BinaryToBCD ; Convert to BCD
- mov cl,al ; To CL
-
- mov ah,3
- int 1Ah ; Set RTC time
- ret ; Return to handler dispatcher
- Write ENDP
-
- BinaryToBCD PROC near ; Convert AL binary to BCD
- xor ah,ah ; Zero hibyte
- mov cl,10
- div cl ; Div 10 - quotient AL, remainder AH
- mov cl,4
- shl al,cl ; Shift quotient to top nibble
- or al,ah ; Combine two nibbles into AL
- ret ; Destroys AX, CL and flags
- BinaryToBCD ENDP
-
- Discard: ; End of resident portion of driver
-
- SignOnMsg DB 13,10,"ATRTC - DOS Device Driver for the AT Real Time Clock"
- DB 13,10,9,"Part of the PC Timing FAQ / Application notes"
- DB 13,10,9,"By K. Heidenstrom (kheidens@actrix.gen.nz)"
- DB 13,10,"$"
-
- InstalledMsg DB 9,"Installed",13,10,"$"
- NoClockMsg DB 9,"Error - RTC not active",13,10,7,"$"
-
- Init PROC near ; Function 0 = Initialise Driver
- mov WORD PTR ds:[bx+14],OFFSET Discard ; Tell DOS where
- mov ds:[bx+16],cs ; free memory starts
-
- mov ax,0F000h ; BIOS code segment
- mov ds,ax
- cmp BYTE PTR ds:[0FFFEh],0FDh ; Check for AT
- pushf ; Preserve result
-
- push cs
- pop ds ; Point DS to our segment address
- ASSUME ds:BinFile
- mov WORD PTR InitPtr,OFFSET Null ; Point INIT at Null proc
-
- mov dx,OFFSET SignOnMsg
- mov ah,9
- int 21h ; Display signon message
-
- popf ; Are we running on an AT?
- jae RTCError ; If not, error!
- mov ah,4
- int 1Ah ; Read date
- mov dx,OFFSET InstalledMsg ; Point to 'installed' message
- jnc NoRTCError ; If RTC is working, skip error stuff
-
- RTCError: mov BYTE PTR Attrib,0 ; Error - clear CLOCK attribute bit
- mov dx,OFFSET NoClockMsg
- NoRTCError: mov ah,9
- int 21h ; Display error or installation message
- ret
- Init ENDP
-
- BinFile ENDS
- END Origin
- -------------------------------- snip snip snip --------------------------------
-
- {TOR} points out that using this driver will result in increased overhead,
- because: "the CLOCK$ device is read VERY often by DOS. I did look at this
- once, and _as_far_as_I_remember_, CLOCK$ is read on every file access".
-
- Though I don't believe this is a problem, the efficiency of this driver in
- cases where frequent file accesses are made could be improved by caching the
- date and time values and the BIOS tick count variable each time the date and
- time are requested, and only re-reading the RTC if the tick count has changed.
- You would use the following logic when the date and time are requested:
-
- Read the current BIOS tick count variable and compare to the stored value.
- If same, copy the cached date and time values into the data area and return.
- If different, copy the current BIOS tick count variable to the stored value,
- read the RTC and recalculate the date and time values, store the new values to
- the variables and copy them to the data area and return.
-
- This method would ensure that the RTC is actually accessed no more often than
- 18.2065 times per second. If frequent file accesses are made, the overhead of
- reading the RTC is avoided for most of them.
-
- Michael Bishop (mxbish2@lookout.ecte.uswc.uswest.com) reports that DOS loses
- time noticeably on his machine which is: "an IBM PS/Note laptop 25MHz 386,
- essentially a PS/2 Model 70/80". While the machine is running, time runs slow.
- After a reboot, the time is restored correctly. This symptom indicates that
- the machine is missing timer ticks (see sections »» 4.1, »» 6.1, and »» 10.15
- for details). Michael was unable to find the IBM driver 'CMOSCLK.SYS' to fix
- this, but reports that ATRTC fixed the problem.
-
- ## 3.4 OTHER BIOS TIME AND ALARM FUNCTIONS
-
- The RTC can generate an alarm at a specific time of day (i.e. every 24 hours)
- until disabled by software. The hardware is more flexible than this (see
- section »» 7.35) but the BIOS function only supports one alarm per day.
- The alarm is signalled via int 4A hex, which is invoked by the BIOS when the
- alarm triggers. Normally int 4Ah points to an IRET. Int 4Ah is invoked under
- interrupt, so the normal considerations for hardware interrupt handlers apply
- (see section »» 6.23 through »» 6.26).
-
- Int 4Ah will normally be called with interrupts disabled, but don't count on
- it. Disable interrupts explicitly if required. The int 4Ah handler must not
- destroy any working registers.
-
- The related BIOS functions are as follows. Note that these functions are only
- supported on the AT and later machines - the PC and XT do not support them.
-
- Set 24-Hour Alarm Time of Day : int 1Ah
- Call with: AH = 06 hex
- CH = Hours (00h to 23h, BCD format)
- CL = Minutes (00h to 59h, BCD format)
- DH = Seconds (00h to 59h, BCD format)
- Returns: Nothing
- Note: When alarm occurs, int 4Ah is invoked
-
- Disable 24-Hour Alarm : int 1Ah
- Call with: AH = 07 hex
- Returns: Nothing
-
- Functions 8, 9, 0Ah, and 0Bh are supported on some IBM models.
- See Ralf Brown's Interrupt List (see section »» 12) for more information.
-
- ## 3.5 OTHER OTHER BIOS TIME FUNCTIONS
-
- The BIOS on the AT and later provides int 15h functions 83h and 86h which use
- the RTC interrupt (1024 interrupts per second on IRQ8, int 70h). See section
- »» 7.35 for more information about the RTC chip, section »» 7.36 for details of
- the RTC interrupt and how to use it, and section »» 7.36.1 for information on
- these BIOS functions.
-
- ## 3.6 THE TIMES THEY ARE A-CHANGIN'
-
- Any technique that makes use of a time taken from the RTC or derived from the
- tick count should take into account the fact that the time can be changed by
- the user, or even by other software. This can cause the time to go forwards
- or backwards slightly, or even jump to a totally different time.
-
- Under real DOS, normally this will only happen to a TSR or a program that shells
- to DOS, where the user may change the time via the TIME command, or a program
- that allows the user to change the time. A networked computer may automatically
- update its time from the server, via the resident network software.
-
- On a machine running a multitasking operating system such as OS/2, Linux, Win95,
- and even Windoze, changing the system date and time in one session will change
- the time in all sessions.
-
- ## 4 USING THE BIOS TICK COUNT VARIABLE
-
- The BIOS tick count variable gives an absolute time reference with a resolution
- of 54.9254 milliseconds.
-
- ## 4.1 THE BIOS TICK COUNT VARIABLE
-
- The BIOS tick count variable is a 32-bit unsigned longword or DWORD, stored at
- low memory address 0040:006C (can also be addressed as 0000:046C), maintained
- by the BIOS's int 8 handler. It contains the number of timer ticks (units of
- 54.9254 ms) since midnight, in the current day. The maximum value in this
- variable is 1800AF hex, so only the bottom 21 bits can ever be nonzero.
-
- The PC and XT have no special real-time clock support in the BIOS, so the tick
- counter is initialised to zero on every reboot. In ATs and later machines, the
- BIOS's power-on initialisation code reads the real-time clock and sets the tick
- count variable to the equivalent number of ticks. See section »» 10.15.
-
- There are approximately 65536 ticks in an hour (65543.4265 to be exact), so the
- high word of the tick count corresponds _approximately_ to the hour of the day.
-
- ## 4.2 CHANGE OF DAY
-
- There are 1,573,042.24 ticks in a day, but the BIOS writers approximated the
- CTC clock to 1.193180 MHz, so the BIOS uses 1,573,040 (001800B0 hex) ticks per
- day. This gives a 1.42166 ppm error (0.123 seconds per day), which is fairly
- insignificant compared to the clock frequency inaccuracy (see section »» 7.2).
-
- The tick count increments up to 001800AF hex, then 'rolls over' to zero at
- midnight. When midnight passes, the BIOS sets the one-byte 'midnight' flag at
- 0040:0070, to 1, indicating that a midnight has passed. Note - some BIOSes may
- indicate change of day by _incrementing_ the midnight flag byte, so that if two
- midnights pass without DOS reading the time, the date could still be updated
- correctly. See section »» 10.15 for details.
-
- ## 4.3 READING AND SETTING THE TICK COUNT
-
- You can read the tick count directly, or request it from the BIOS via int 1Ah.
-
- Get Tick Count : int 1Ah
- Call with: AH = 00 hex
- Returns: CX = High word of tick count
- DX = Low word of tick count
- AL = Midnight-passed flag
- Notes: This call clears the midnight flag byte.
- Notes: Do not use this call in an application - see below
-
- Set Tick Count : int 1Ah
- Call with: AH = 01 hex
- CX = High word of tick count
- DX = Low word of tick count
- Notes: This call clears the midnight flag byte.
-
- The DOS CLOCK$ device driver uses the Get Tick Count function, int 1Ah, function
- 0, and relies on the midnight flag returned by this function to detect a change
- of day. User programs should not use these two BIOS functions, because if the
- program calls the function just after midnight, it will see the midnight flag,
- and the midnight flag will be cleared, so DOS will miss out on seeing the change
- of day, and will not increment the date. See sections »» 10.15 and »» 10.16.
- This problem would be solved if DOS used the real-time clock for timekeeping
- (see section »» 3.3 for a DOS device driver that uses the real-time clock).
-
- It is safer and more efficient to read (and write) the count directly at its
- location in low memory. The tick count is 'volatile', and must be accessed
- with an indivisible operation (using a 32-bit register such as EAX), or with
- interrupts disabled. If you access the loword and hiword separately without
- disabling interrupts around the two accesses, a tick interrupt could come
- along and modify the tick count variable between the two reads or writes.
- See section »» 4.5 for details.
-
- ## 4.4 SPECIAL REQUIREMENTS - NONE
-
- The great advantage of timing using the BIOS tick count, is that it makes no
- changes to the system, i.e. it doesn't change the hardware setup, or modify any
- interrupt vectors. This simplifies the code, and means that if the program is
- terminated (by Ctrl-Break, or a Divide Overflow, or by the user replying 'A' to
- the Abort, Retry, Ignore prompt), no special clean-up is required.
-
- ## 4.5 SAMPLE PROGRAM: READING THE TICK COUNT
-
- The function read_bios_tick_count() reads and returns the BIOS tick count. The
- function has_tick_occurred() detects whether the tick count has changed since
- the last time that function was called. It returns TRUE on the initial call.
- It does not report _how_many_ timer ticks occurred between calls.
-
- Notice that read_bios_tick_count() explicitly disables interrupts around the
- read of the 32-bit tick count value. Even though the tick count variable is
- declared as volatile, the compiler (Borland C++ 2.0) generates two 16-bit MOV
- instructions without disabling interrupts. If an interrupt occurred between
- the two MOV instructions, an incorrect value will be read. Apparently this is
- not a bug, it is because the compiler doesn't know how to safely read 'volatile'
- variables. Hmm. I'd say if it's not a bug, it's definitely a mis-feature.
-
- If the compiler can use the 32-bit registers (compiling for protected mode, or
- compiling with 32-bit code under DOS, this problem does not (or should not!)
- occur. Michael Mauch (mauch@uni-duisburg.de) found that Borland C++ 4.0 does
- use a 32-bit MOV instruction if 32-bit code generation is enabled via #pragma
- option -3 or #pragma option -4.
-
- Dr. John Stockton (see section »» 1.7) reports that this problem also exists in
- Borland Pascal 7 when a signed long variable (BP7 doesn't have _unsigned_ longs)
- is loaded from the tick count variable, as the tick count is read non-atomically
- with two 16-bit accesses. Disabling interrupts around the load prevents the
- problem described above.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #2
- Demonstrates reading the BIOS tick count
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE2.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample2.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <stdio.h> /* Pass go, add printf(), program is 8K already :-) */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
-
- unsigned long read_bios_tick_count(void) {
- unsigned long ct;
- asm pushf; /* Preserve interrupt flag */
- asm cli; /* Needed even though tick count is volatile */
- ct = * BIOS_TICK_COUNT_P;
- asm popf; /* Restore interrupt flag */
- return ct;
- }
-
- int has_tick_occurred(void) {
- static unsigned long old_tick_count = 0xFFFFFFFFL; /* Invalid */
- if (read_bios_tick_count() != old_tick_count) { /* Changed? */
- old_tick_count = read_bios_tick_count();
- return TRUE;
- }
- return FALSE; /* No change */
- }
-
- void main(void) {
- unsigned int n = 0;
-
- printf("Sample program #2 - Demonstrates reading the BIOS tick count variable\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
-
- while (n < 18) /* Stop after one second */
- if (has_tick_occurred())
- printf("Tick %d: BIOS tick count variable = %ld\n",
- ++n, read_bios_tick_count());
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 4.6 SAMPLE CODE: OPTIMISED FUNCTION TO READ THE TICK COUNT
-
- This is a more optimal coding of read_bios_tick_count() in assembler. I chose
- to disable interrupts and read the loword and hiword separately, rather than
- using LES or LDS (indivisible operations) because it is not good practice to
- load a segment register with a value which is not a real segment-paragraph.
-
- Of course if your code requires a 386 or higher, you can just load an extended
- (32-bit) register (e.g. EAX) in one single indivisible operation.
-
- -------------------------------- snip snip snip --------------------------------
- ; Function to read the BIOS tick count (C-callable)
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- _read_bios_tick_count PROC near ; or FAR for far code model
- ; unsigned long read_bios_tick_count(void);
- push ds ; Preserve data segment
- pushf ; Keep interrupt flag
- xor ax,ax ; Zero
- mov ds,ax ; Address BIOS data area
- cli ; Don't want a tick to interrupt us
- mov ax,ds:[46Ch] ; Get loword of count
- mov dx,ds:[46Eh] ; Get hiword of count
- popf ; Restore interrupt flag as provided
- pop ds ; Restore data segment
- ret ; Return tick count in DX|AX
- _read_bios_tick_count ENDP
- -------------------------------- snip snip snip --------------------------------
-
- ## 4.7 SAMPLE PROGRAM: USING THE TICK COUNT FOR TIMEOUT CHECKING
-
- This example demonstrates two independent timeout counters using the BIOS tick
- count variable. The timeout counter record consists of the starting tick count,
- the number of ticks in the timeout period, and a flag which can be used to
- report the transition to the timed-out state.
-
- set_timeout() sets up a timeout counter. The state of the timeout can then be
- requested using is_timedout() and just_timedout(). is_timedout() returns TRUE
- if the current time is outside the timeout period specified by the counter.
- just_timedout() returns TRUE the first time it is called after the timeout
- expires, and from then on, returns FALSE until a new timeout is configured.
-
- The timeout may occur up to one tick earlier than expected, depending on the
- synchronisation between setting the timeout, and the actual timer tick. A one
- tick timeout will time out on the next tick that occurs after the timeout was
- set up, so if the timeout is set just after a tick has occurred, the timeout
- will occur nearly 54.9254 ms later, but if the timeout is set just before a
- tick, the timeout will occur almost immediately. See section »» 10.10 for
- more details.
-
- Because the tick count restarts at midnight, leaving a timeout active for a
- whole day will cause the timeout state to change. For example a ten minute
- timeout will expire after ten minutes, but every day thereafter, from the time
- that the timeout started, the timeout function will report not timed out for
- ten minutes.
-
- This demo program uses two timeout counters, and waits for ten keypresses. One
- timeout counter is used as a global timeout for the whole program, set to 20
- seconds. The other timeout is used as a timeout for each individual keypress.
- To avoid both timeouts, you must press any key ten times within a total of 20
- seconds, with no more than four seconds elapsing between the keys. So, it's a
- demo! I didn't say it would be useful. Call it a game of skill :-)
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #3
- Demonstrates multiple timeouts using the BIOS tick count
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE3.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample3.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define NTIMEOUTS 2 /* Set this to however many timeouts you need */
-
- #define GLOBAL_TIMEOUT 0 /* Counter number to use for global timeout */
- #define CHAR_TIMEOUT 1 /* Counter to use for per-character timeout */
-
- #define FALSE 0
- #define TRUE 1
-
- unsigned long timeoutstart[NTIMEOUTS]; /* Starting tick value per timeout */
- unsigned int timeoutlength[NTIMEOUTS]; /* Timeout period (ticks) per timeout */
- unsigned int timeoutflag[NTIMEOUTS]; /* Flags for timeout state */
-
- #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
-
- #define TICK_WRAP 0x001800B0L /* Past last value of tick count */
-
- unsigned long read_bios_tick_count(void) {
- unsigned long ct;
- asm pushf;
- asm cli;
- ct = * BIOS_TICK_COUNT_P;
- asm popf;
- return ct;
- }
-
- /* tick_diff(), returns the difference between two timer tick counts. This
- is just new value minus old value, except if the period crosses midnight. */
-
- unsigned long tick_diff(unsigned long start_tick, unsigned long now_tick) {
- signed long diff;
- if (start_tick >= TICK_WRAP || now_tick >= TICK_WRAP)
- return 0xFFFFFFFFL; /* Invalid */
- diff = now_tick - start_tick;
- if (diff < 0)
- diff += TICK_WRAP;
- return (unsigned long) diff;
- }
-
- /* Set a timeout counter for timeout after a specific number of ticks */
-
- void set_timeout(unsigned int timeoutnum, unsigned int timeoutticks) {
- if (timeoutnum >= NTIMEOUTS)
- return;
- timeoutstart[timeoutnum] = read_bios_tick_count(); /* Start time */
- timeoutlength[timeoutnum] = timeoutticks; /* Duration */
- timeoutflag[timeoutnum] = FALSE;
- return;
- }
-
- /* Returns whether the nominated counter is in the timed-out state. After the
- timeout has expired, this function will return TRUE, until a new timeout
- period is set. Do not leave timeouts active for periods approaching one
- day, as this will cause the timeout state to be incorrectly reported as
- FALSE for the same period of each day. */
-
- int has_timedout(unsigned int timeoutnum) {
- if (timeoutflag[timeoutnum])
- return TRUE; /* Latch the timed-out state */
- return (tick_diff(timeoutstart[timeoutnum], read_bios_tick_count()) >= timeoutlength[timeoutnum]);
- }
-
- /* Test whether a counter has just timed out. Returns TRUE only the
- first time it is called after the timeout occurs. */
-
- int just_timedout(unsigned int timeoutnum) {
- if (timeoutflag[timeoutnum] == TRUE) /* Already reported timeout */
- return FALSE;
- if (has_timedout(timeoutnum)) { /* Timeout has expired */
- timeoutflag[timeoutnum] = TRUE;
- return TRUE;
- }
- return FALSE; /* Timeout has not expired yet */
- }
-
- void main(void) {
- unsigned int n, key;
-
- printf("Sample program #3 - Demonstrates multiple timeouts using the BIOS tick count\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Press any key ten times\n");
- printf("The timeout on each character is four seconds\n");
- printf("The overall timeout on all ten characters is twenty seconds\n\n");
-
- set_timeout(GLOBAL_TIMEOUT, 364); /* Global timeout 20 sec */
- for (n = 0; n < 10; ++n) { /* Read ten characters */
- set_timeout(CHAR_TIMEOUT, 73); /* Char timeout 4 sec */
- while (TRUE) {
- if (just_timedout(CHAR_TIMEOUT)) {
- printf("Timed out on single character\n");
- exit(1);
- }
- if (just_timedout(GLOBAL_TIMEOUT)) {
- printf("Global timeout expired\n");
- exit(2);
- }
- if (bioskey(1)) {
- key = bioskey(0);
- break;
- }
- }
- printf("Key pressed: %c\n", key);
- }
- printf("Neither timeout expired; normal program termination");
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 4.8 SIMPLE DELAYS USING THE BIOS TICK COUNT
-
- A simple way to implement delays of about 0.1 seconds or longer with one tick
- resolution, or perform timeout checking, is to provide a function that waits
- for a tick to occur, such as the following function:
-
- -------------------------------- snip snip snip --------------------------------
- void wait_next_tick(void) {
- static unsigned int last_tick_loword;
- unsigned int now_tick_loword;
- do {
- now_tick_loword = * ((volatile unsigned int far *) 0x0040006CL);
- } while (now_tick_loword == last_tick_loword);
- last_tick_loword = now_tick_loword;
- return;
- }
- -------------------------------- snip snip snip --------------------------------
-
- This function can then be called in a loop, e.g.
-
- for (n = 0; n < 10; ++n)
- wait_next_tick();
-
- to implement a delay of the desired number of ticks. A modified method can
- be used to implement timeout checking with regular polling of some input
- device such as a serial port buffer, or the keyboard.
-
- There is no need to use the hiword of the BIOS tick count variable; just
- checking for a change in the loword is enough to detect that a tick has
- occurred.
-
- ## 5 SPECIAL SOFTWARE PRECAUTIONS
-
- If your program intercepts any interrupt vectors (e.g. int 8 or int 1Ch), or
- reprograms the RTC or other hardware into a strange mode, it must restore
- hardware states and interrupt vectors (i.e. clean up) if terminated by DOS,
- or risk having its interrupt handlers overwritten by another program and
- causing a system crash or causing incorrect operation due to the hardware
- being in the wrong state.
-
- You should handle the following interrupts:
-
- ■ DOS Ctrl-C interrupt
- ■ DOS Critical Error interrupts
- ■ Divide Overflow interrupt (optional)
-
- Here are the gory details. For more details try DOS technical books such as
- the MS-DOS Encyclopedia or Ralf Brown's venerated Interrupt List (see section
- »» 12 for details of both of these references).
-
- ## 5.1 THE CTRL-C AND CTRL-BREAK INTERRUPTS
-
- Int 23h is the DOS Ctrl-C interrupt. It is invoked by DOS whenever a Ctrl-C
- character (ASCII code 3) is detected in the keyboard input stream. When the
- Ctrl-Break combination is pressed, the BIOS issues int 1Bh (the Ctrl-Break
- interrupt), and DOS's int 1Bh handler sets an internal flag in DOS that causes
- a faked Ctrl-C to appear in the input.
-
- Thus, by trapping Ctrl-C, you are trapping Ctrl-Break too, except that Ctrl-C
- will only be registered when DOS input is read, while Ctrl-Break is generated
- as soon as the keystroke is accepted. Also, if input redirection is used, the
- Ctrl-C interrupt may not be registered properly, depending on the 'BREAK='
- setting in CONFIG.SYS.
-
- ## 5.2 HANDLING THE CTRL-C INTERRUPT
-
- You can just replace the default int 23h handler using setvect() (DOS function
- 25h), there's no need to save the previous vector contents because DOS will
- restore the vector for you when the program exits or is terminated. However,
- if you intercept int 1Bh as well as int 23h, DOS will not restore int 1Bh
- when your program terminates, so your program will have to do this itself.
-
- Typical actions for a Ctrl-C interrupt handler include:
-
- ■ Do nothing and rely on the Ctrl-C appearing in the keyboard input
- stream to the program (this will only happen if the program reads
- its keyboard input via DOS, not via the BIOS),
- ■ Set a flag which will be checked by the program's mainline and will
- cause the mainline to take some appropriate action (e.g. clean up
- and terminate the program),
- ■ Call a general 'user interruption' function inside the main portion
- of the program, which registers the Ctrl-C request, and/or takes an
- appropriate action,
- ■ Restore interrupt vectors, restore normal hardware states, clean up,
- and terminate the program immediately, by itself.
-
- All DOS functions can be called from within a Ctrl-C interrupt handler. Some C
- library functions may not be safe to call - for instance, the function which was
- reading the DOS keyboard input when the Ctrl-C was detected, will be in progress
- and might not be re-entrant - see your compiler's library reference for details.
-
- On entry to the Ctrl-C interrupt handler, interrupts will be disabled, and it
- would normally be appropriate to enable them, using enable() or STI, unless the
- handler will always return quickly.
-
- If the handler returns control to DOS, an IRET instruction should be used.
- There is no return value. General registers may be modified by the Ctrl-C
- handler. Alternatively, the handler may call DOS to terminate the program
- (e.g. via DOS function 4Ch, terminate with return code).
-
- ## 5.3 THE CRITICAL ERROR INTERRUPT
-
- Int 24h is the DOS Critical Error interrupt, and is issued by DOS when a device
- driver indicates a critical failure.
-
- The default critical error handler issues the familiar "Abort, Retry, Ignore?"
- prompt. You can replace the default handler using setvect() (DOS function 25h).
- DOS will restore the vector for you when the program exits or is terminated.
-
- On entry to the int 24h handler, registers AX, SI, DI, BP contain information
- about the nature of the critical error, and the stack contains the values in all
- registers as provided to the int 21h call which caused the critical error to
- occur, as well as the return address for the int 21h call. For these reasons,
- int 24h handlers are usually written in assembler.
-
- ## 5.4 CRITICAL ERROR HANDLER PARAMETERS
-
- On entry to the int 24h handler, the stack is arranged thus:
-
- [SS:SP+0] IP (PC) of return address for int 24h handler
- [SS:SP+2] CS of return address for int 24h handler
- [SS:SP+4] Flags for return of int 24h handler
- [SS:SP+6] AX as provided to int 21h invocation
- [SS:SP+8] BX as provided to int 21h invocation
- [SS:SP+0Ah] CX as provided to int 21h invocation
- [SS:SP+0Ch] DX as provided to int 21h invocation
- [SS:SP+0Eh] SI as provided to int 21h invocation
- [SS:SP+10h] DI as provided to int 21h invocation
- [SS:SP+12h] BP as provided to int 21h invocation
- [SS:SP+14h] DS as provided to int 21h invocation
- [SS:SP+16h] ES as provided to int 21h invocation
- [SS:SP+18h] IP (PC) of return address for int 21h invocation
- [SS:SP+1Ah] CS of return address for int 21h invocation
- [SS:SP+1Ch] Flags for return from int 21h invocation
-
- On entry to the int 24h handler, BP:SI contain the segment:offset address of
- the device driver header of the device which flagged the critical error.
-
- The high eight bits of the DI register are undefined. The lower eight
- bits of DI contain the error description, as follows:
-
- 0 = Write-protected disk, 1 = Unknown unit, 2 = Drive not
- ready, 3 = Invalid command, 4 = Data error, 5 = Invalid request
- structure length, 6 = Seek error, 7 = Non-DOS disk, 8 = Sector
- not found, 9 = Out of paper (printer), 10 = Write fault,
- 11 = Read fault, 12 = General failure, 15 = Invalid disk
- change (DOS 3.0 and later).
-
- Many of these error codes are not applicable to character devices.
-
- If bit 7 of AH is set, the critical error occurred on a character device (e.g.
- PRN or AUX), and all other bits of AX are undefined.
-
- If bit 7 of AH is clear, the critical error occurred on a block device (i.e. a
- disk drive), and the error location is described by the remaining bits in AX.
- AL contains the drive designator minus 41 hex (i.e. 0 means drive A, 1 means
- drive B, 2 means drive C, etc). AH describes the error location, as follows:
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . 0 (Error occurred on block device)
- . * . . . . . . Not used
- . . * . . . . . "Ignore" allowed? (0 = no, 1 = yes) (3.1+)
- . . . * . . . . "Retry" allowed? (0 = no, 1 = yes) (3.1+)
- . . . . * . . . "Fail" allowed? (0 = no, 1 = yes) (3.1+)
- . . . . . * * . Location: 00=DOS, 01=FAT, 10=Root, 11=Files
- . . . . . . . * Read or Write operation (0 = read, 1 = write)
-
- Bits 3, 4, and 5 are only meaningful if the DOS version is 3.1 or later.
- The DOS version may be checked from inside the critical error handler, using
- DOS function 30 hex, or it may be determined by startup code and stored in a
- global variable accessible by the critical error handler.
-
- ## 5.5 CRITICAL ERROR HANDLER OPERATION
-
- The critical error handler may use DOS functions 01 through 0Ch (the old CP/M
- character I/O functions). It may also use DOS functions 30h and 59h (request
- DOS version, and request extended error information). Other DOS functions may
- NOT be called, as DOS is mostly non-reentrant.
-
- The critical error handler must preserve all register values, except the flags
- (presumably) and AL, which is used to specify the action for DOS to take upon
- return from the handler, as follows:
-
- 0 = Ignore
- 1 = Retry
- 2 = Abort
- 3 = Fail current function
-
- Ideally, a critical error handler built in to a program should also deallocate
- any other resources that that program might have allocated, such as EMS and/or
- XMS memory. Temporary files cannot be safely deleted because the DOS file
- functions must not be called. Possibly if the handler is going to abort the
- program anyway, it may be safe to call these functions. If anyone has detailed
- info, please let me know. (*)
-
- ## 5.6 THE DIVIDE OVERFLOW INTERRUPT
-
- The divide overflow interrupt is int 0. It is generated by the processor when
- the quotient of a signed or unsigned integer division (IDIV or DIV instruction)
- would exceed the size of the register into which it would be placed.
-
- DOS's default divide overflow handler issues the message "Divide overflow" and
- terminates the current application, giving the program no chance to restore
- interrupt vectors, hardware states, or allocated resources, or close files, etc.
-
- Generally if a divide overflow occurs, the user should reboot their system. As
- a result (or perhaps the cause) of this, most programs do not provide their own
- divide overflow interrupt handlers.
-
- If you wish to handle divide overflows, I would suggest using direct writes to
- the interrupt vector table in low memory to restore all intercepted vectors
- (except int 23h and 24h, which will be restored by DOS), restoring the hardware
- state directly, and perhaps deallocating any resources such as EMS and/or XMS,
- then calling DOS function 4Ch to terminate the program.
-
- It might be possible to write a divide overflow handler which resumes execution
- after loading an appropriate value into the result register. This requires
- scanning the offending instruction, and a detailed knowledge of the operation
- of the various x86 processors, and is left as an exercise for the reader :-)
-
- ## 5.7 ERROR HANDLING SYSTEM
-
- The error handling system I have used in the sample programs uses a function
- prototyped as follows:
-
- void abort_cleanup(int dos_is_safe);
-
- This function is responsible for performing as much cleanup as possible at
- program exit time. The dos_is_safe parameter specifies whether DOS functions
- may be safely used by the cleanup function. This parameter will be FALSE if the
- function is called from within the critical error handler or a divide overflow
- handler, and TRUE if the function is called from the Ctrl-C handler or by the
- program itself during cleanup for an orderly exit.
-
- abort_cleanup should not exit to DOS itself. This will be done by the caller.
-
- If dos_is_safe is FALSE, your abort_cleanup() function should not call any DOS
- functions. Interrupt vectors should be restored using direct accesses into the
- interrupt table in low memory (though this technique is frowned upon).
-
- Depending on the types of cleanups required, DOS may _have_ to be called. In
- certain circumstances (e.g. after a divide overflow), abort_cleanup() may crash
- the machine in its attempt to clean up properly. On the other hand, if it did
- not attempt to clean up properly, the machine might be left in an unstable state
- anyway. You will have to weigh up the pros and cons when deciding how much to
- try to clean up if dos_is_safe is FALSE. Perhaps you should do the most
- critical and/or most likely to succeed cleanups first.
-
- ## 5.8 SAMPLE CODE MODULE: CRITICAL ERROR HANDLER MODULE
-
- -------------------------------- snip snip snip --------------------------------
- NAME CRIT_ERR
-
- ; Rudimentary critical error handler module
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This module provides rudimentary critical error (int 24h) handling for DOS
- ; application programs. It is callable from Borland C. This file is written
- ; for small model (near code, near data). You can change the FAR_CODE equate
- ; to hopefully make it compatible with other memory models.
- ;
- ; This is a minimal implementation for demonstration purposes.
- ;
- ; Upon startup, the application should call crit_err_intercept() to install the
- ; new critical error handler. No corresponding uninstallation is required.
- ;
- ; If the user selects the Abort option to the "Abort, Retry, Ignore" prompt,
- ; the abort_cleanup() function (which is provided externally) is called, with
- ; its dos_is_safe parameter set to FALSE. This function performs as much
- ; cleanup as possible (restoring interrupt vectors, restoring hardware states,
- ; setting normal text video mode, and deallocating resources such as EMS and
- ; XMS memory. It would also delete temporary files and close any open files if
- ; dos_is_safe were TRUE. After abort_cleanup() returns, the critical error
- ; handler returns the Abort code to DOS, which will then abort the program.
- ;
- ; Save this file to CRIT_ERR.ASM and assemble with:
- ; masm /Mx crit_err;
- ; or
- ; tasm /mx crit_err;
- ; to produce CRIT_ERR.OBJ which can be linked into the user program.
-
- FALSE EQU 0
- TRUE EQU 1
-
- FAR_CODE EQU FALSE ; TRUE for far code models
-
- ; void crit_err_intercept(void);
- PUBLIC _crit_err_intercept
- ; unsigned int is_at_crit_prompt(void);
- PUBLIC _is_at_crit_prompt
-
- ; void abort_cleanup(int dos_is_safe);
- IF FAR_CODE
- EXTRN _abort_cleanup : FAR
- ELSE
- EXTRN _abort_cleanup : NEAR
- ENDIF
-
- _DATA SEGMENT
- _DATA ENDS
-
- DGROUP GROUP _DATA
-
- _TEXT SEGMENT PARA PUBLIC 'CODE'
- ASSUME cs:_TEXT
-
- ; Data - in code segment (naughty naughty)
-
- I24_IP DW 0 ; IP for return from int 24h intercept
- I24_CS DW 0 ; CS for same
- I24_FL DW 0 ; Flags for same
-
- In_Crit DB 0 ; Flag whether currently at int 24h prompt
-
- IF FAR_CODE
- _crit_err_intercept PROC far
- ELSE
- _crit_err_intercept PROC near
- ENDIF
-
- ; This function intercepts interrupt 24h, and replaces the DOS default int 24h
- ; handler with the new handler, crit_err_handler. This function should be
- ; called ONCE and ONLY ONCE at program startup. No corresponding restore-
- ; interrupt function is required.
-
- mov ax,3524h ; Request int 24h
- int 21h
- mov cs:[O24_IP],bx ; Self-modifying code?
- mov cs:[O24_CS],es ; Where? I didn't see it :-)
- push ds
- push cs
- pop ds
- mov dx,OFFSET _TEXT:crit_err_handler
- mov ax,2524h ; Set int 24h
- int 21h
- pop ds
- ret
- _crit_err_intercept ENDP
-
- IF FAR_CODE
- _is_at_crit_prompt PROC far
- ELSE
- _is_at_crit_prompt PROC near
- ENDIF
-
- ; This function returns the status of the In_Crit flag, and should be called
- ; by the Ctrl-C interrupt handler (if any) to check that the Ctrl-C was not
- ; pressed while at the Abort, Retry, Ignore prompt. The function returns
- ; FALSE if not at the prompt, or TRUE if at the prompt. If it returns TRUE,
- ; the Ctrl-C handler is not safe to call general DOS functions.
-
- mov al,cs:[In_Crit]
- xor ah,ah
- ret
- _is_at_crit_prompt ENDP
-
- crit_err_handler PROC far
-
- ; This function handles interrupt 24 hex, the DOS Critical Error interrupt.
- ; It calls the original DOS interrupt 24h handler, and checks the returned
- ; action code.
- ; If the action code is 2 (abort), it calls abort_cleanup() (provided by the
- ; application), passing a FALSE value for the dos_is_safe parameter. In
- ; either case, it then returns the user-specified action code to DOS.
- ;
- ; See documentation above for details of the abort_cleanup() function.
-
- pop cs:[I24_IP] ; IP of return address of int 24h
- pop cs:[I24_CS] ; CS of return address of int 24h
- pop cs:[I24_FL] ; Flags of int 24h invocation
- mov cs:[In_Crit],1 ; Set flag
- pushf ; Simulate an INT
- DB 9Ah ; CALL xxxx:xxxx
- O24_IP DW 0 ; Offset of call (modified)
- O24_CS DW 0FFFFh ; Segment of call (modified)
- mov cs:[In_Crit],0 ; Clear flag
- cmp al,2 ; Did user choose Abort?
- jne NotAbort ; If not
- push es ; If so, call abort_cleanup()
- push ds
- push di
- push si
- push bp
- push dx
- push cx
- push bx
- push ax
- mov ax,SEG DGROUP
- mov ds,ax ; Set up DS for call to C function
- xor ax,ax ; dos_is_safe is FALSE!
- push ax
- call _abort_cleanup
- pop ax ; Discard parameter
- ; mov ax,0E07h ; Enable these lines during debugging
- ; xor bx,bx ; to generate a beep after your
- ; int 10h ; abort_cleanup() function completes
- pop ax
- mov al,2 ; Restore the Abort code
- pop bx
- pop cx
- pop dx
- pop bp
- pop si
- pop di
- pop ds
- pop es
- NotAbort:
- push cs:[I24_FL] ; Flags of int 24h invocation
- push cs:[I24_CS] ; CS of return address of int 24h
- push cs:[I24_IP] ; IP of return address of int 24h
- iret
-
- crit_err_handler ENDP
-
- _TEXT ENDS
- END
- -------------------------------- snip snip snip --------------------------------
-
- Gian Uberto Lauri (saint@dei.unipd.it) sent me a modified version of this
- module with the names changed to support the Borland C++ 3.1 compiler when
- compiling a C++ program. Borland C++ encodes the parameter types in the
- function name ("mangling"). Gian had to change the names of the functions
- as follows:
-
- _crit_err_intercept --> @crit_err_intercept$qv
- _is_at_crit_prompt --> @is_at_crit_prompt$qv
- _abort_cleanup --> @abort_cleanup$qi
-
- These names must be changed both at the PUBLIC or EXTRN declarations, and at
- the actual PROC and ENDP lines. These changes are specific to Borland C++.
- Other C++ compilers will handle this differently.
-
- ## 6 INTERRUPTS
-
- An interrupt is an interruption to the processor, that causes it to stop what
- it is doing and jump to a specially written subroutine, known as an interrupt
- handler, interrupt routine, or interrupt service routine (ISR).
-
- There are three types of interrupts - processor-generated interrupts, external
- hardware interrupts, and software interrupts. Processor-generated interrupts
- are generated internally by the processor (Intel 80x86) in certain conditions,
- such as a division overflow (see section »» 5.6). External hardware interrupts
- are generated by IRQs, and are described shortly. Software interrupts are
- invoked by software, and are generally used for calling system functions, e.g.
- BIOS functions, DOS functions, mouse functions, EMS functions, etc.
-
- Interrupts are identified by an interrupt number, in the range 0 to 0FFh.
-
- ┌───────────────────────┬───────────────────────────────────────────────┐
- │ Interrupt numbers │ Interrupt type │
- ├───────────────────────┼───────────────────────────────────────────────┤
- │ 0,1,2,3,4 │ Processor │
- │ 5,6,7 │ Software and processor │
- │ 8-0Fh │ Hardware (IRQ0-7) and processor │
- │ 10h-6Fh │ Software (some are also processor interrupts) │
- │ 70h-77h │ Hardware (IRQ8-15) │
- │ 78h-0FFh │ Software │
- └───────────────────────┴───────────────────────────────────────────────┘
-
- Some low-numbered interrupts have a split personality, because IBM ignored
- Intel's "reserved for processor" comment on the first 32 interrupts. The
- original 8086/8088 only used ints 0, 1, 2, 3, and 4 for processor interrupts,
- so IBM used ints 5 and upwards for hardware and software interrupts. With
- later x86 processors, Intel reclaimed their reserved interrupts, requiring
- special support in the EMM386 driver to handle these interrupts properly.
- See section »» 6.7 for details.
-
- Tor Sjowall {TOR} points out that these conflicts only occur in real mode and
- virtual 86 mode. In protected mode, there is no such conflict - the processor
- interrupts have their Intel defined functions, the hardware interrupts are
- vectored through different interrupts, and the software interrupts are not
- relevant (since they relate to DOS and BIOS, which are not protected mode
- programs).
-
- Software interrupts 23h and 24h (Ctrl-C and Critical Error) are described in
- section »» 5 and subsections. Software interrupt 1Ch and hardware int 8 are
- the timer tick interrupts, and are described in section »» 6.1.
-
- ## 6.1 THE TIMER TICK INTERRUPTS
-
- Interrupt 8 and interrupt 1C hex are the timer tick interrupts.
-
- Int 8 is a hardware interrupt, invoked directly by IRQ0, from CTC channel zero,
- and is the highest priority IRQ (unless interrupt priorities have been changed
- from the BIOS defaults). The BIOS POST sets int 8 to point to the BIOS's int 8
- interrupt service routine, traditionally located at F000:FEA5, which performs
- the delayed floppy disk motor turn-off and updates the system time-of-day.
- Device drivers and TSRs often intercept this interrupt, so often the vector
- won't point directly to the BIOS.
-
- Int 1C hex is issued (i.e. generated) by the BIOS's int 8 service routine, and
- normally points to an IRET instruction in the BIOS. Int 1Ch is intended to be
- used by application programs which require a regular interrupt source.
- Some TSRs also hook this interrupt - see section »» 6.35 for details.
-
- ## 6.2 INTERRUPT VECTOR TABLE
-
- The interrupt vector table, or IVT, is a reserved area of RAM occupying the
- bottom kilobyte of main memory, i.e. from 0000:0000 to 0000:03FF. This is in
- real mode or virtual 8086 mode, under DOS. In protected mode this is probably
- completely different. (*)
-
- Each interrupt has a corresponding four-byte far code pointer, located at
- interrupt number x 4 bytes into the IVT, which points to the interrupt service
- routine that will be invoked when that interrupt is registered by the processor.
-
- For example, here is a dump of the first 128 bytes of the IVT on my machine:
-
- 0000:0000 1A 00 70 00 05 00 70 00 1B 2C 5D 57 05 00 70 00 ..p...p..,]W..p.
- 0000:0010 05 00 70 00 54 FF 00 F0 4C E1 00 F0 6F EF 00 F0 ..p.T..pLa.poo.p
- 0000:0020 57 01 80 E6 AD 2B 5D 57 6F EF 00 F0 45 10 1F CF W..f-+]Woo.pE..O
- 0000:0030 6F EF 00 F0 6F EF 00 F0 57 EF 00 F0 6F EF 00 F0 oo.poo.pWo.poo.p
- 0000:0040 C6 01 80 E6 4D F8 00 F0 41 F8 00 F0 C0 05 A2 D1 F..fMx.pAx.p@."Q
- 0000:0050 39 E7 00 F0 18 00 55 02 20 01 B3 E5 D2 EF 00 F0 9g.p..U. .3eRo.p
- 0000:0060 D4 E3 00 F0 65 0F A2 D1 6E FE 00 F0 64 06 70 00 Tc.pe."Qn~.pd.p.
- 0000:0070 1B 91 A1 03 A4 F0 00 F0 22 05 00 00 6E 42 00 C0 ..!.$p.p"...nB.@
-
- The vector for interrupt 1C hex starts at 1Ch x 4, which is 70 hex. In the
- above vector table contents, the vector at 0000:0070 points to 03A1:911B, so
- every time int 1Ch is issued, the processor will jump to 03A1:911B and execute
- the ISR that starts at that address.
-
- ## 6.3 INTERCEPTING AN INTERRUPT
-
- To take control of an interrupt, use the getvect() and setvect() functions or
- DOS functions 35 hex and 25 hex. If necessary, you can directly access the
- interrupt vector table in low memory (see section »» 6.2). This may be required
- if DOS cannot safely be called - for example, in a critical error handler (see
- section »» 5.3). Interrupts MUST be locked out while any direct manipulation
- of this type is performed.
-
- Start by requesting the contents of the interrupt vector, using getvect() or
- DOS function 35 hex. This gives a far code pointer, which must be stored to be
- reinstated when your program terminates. The stored 'old interrupt' vector is
- also used for interrupt chaining (section »» 6.31). Then, set the interrupt
- vector to point to your new handler, using setvect() or DOS function 25 hex,
- and away you go.
-
- See section »» 5 and subsections for details of intercepting the DOS Ctrl-C and
- critical error interrupts and the divide overflow interrupt, which must be done
- to ensure that your program reinstates the original interrupt owner upon exit.
-
- ## 6.4 INTERRUPT HARDWARE
-
- Hardware interrupts are known as IRQs (interrupt requests). They interrupt the
- processor from its current task and cause it to jump to an interrupt handler,
- aka interrupt service routine (ISR). The processor has only one IRQ input,
- which is expanded by an 8259 PIC (programmable interrupt controller).
-
- The PC and XT have one PIC, which provides IRQ0-7. IRQ0 and 1 are the timer
- tick and keyboard interrupts, respectively. IRQ2 through IRQ7 are available
- on the slot bus, for use by peripheral cards.
-
- The AT has two PICs - the primary PIC, which is equivalent to the single PIC
- on the PC and XT, and the secondary PIC (also sometimes called the slave PIC).
- The third input (IRQ2) on the primary PIC is known as the 'chain' or 'cascade'
- or 'slave' interrupt on the AT, because it is the method by which the secondary
- PIC issues an interrupt request. The slot bus connection that was IRQ2 on the
- PC is replaced by IRQ9 on the AT and later machines (ISA bus).
-
- The two PICs and their interconnection are shown in Figure 2 in the FIGURES
- archive.
-
- Each PIC is responsible for prioritising its incoming interrupt requests, and
- issuing an interrupt request signal to the processor - either directly (in the
- case of the primary PIC) or via the primary PIC (in the case of the secondary
- PIC).
-
- Hardware interrupts are registered on the rising edge of the PIC input, which
- corresponds to a rising edge of the IRQ line on the slot bus of ISA machines.
- This is known as rising edge triggered interrupts. Level triggered interrupts,
- particularly active low level triggered interrupts, are more sensible for most
- applications, and EISA machines are apparently configurable for either edge-
- triggered or level-triggered operation. The MicroChannel Architecture (MCA)
- bus, used in IBM PS/2 machines, uses level triggered interrupts.
-
- The PICs are accessed via two I/O locations. The primary PIC appears at I/O
- addresses 20h and 21h, the secondary PIC (not present on PC and XT) appears at
- I/O addresses 0A0h and 0A1h. The lower address is the command/status register,
- the upper address is the interrupt mask register (IMR).
-
- ## 6.5 IRQ TO INTERRUPT MAPPING
-
- The default mapping between hardware interrupt requests (IRQs) and interrupts
- is set up by the BIOS POST, and is as follows.
-
- ┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
- │ IRQ Int │ IRQ Int │ IRQ Int │ IRQ Int │
- ├───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤
- │ 0 8 │ 4 0Ch │ 8 70h │ 12 74h │
- │ 1 9 │ 5 0Dh │ 9 71h │ 13 75h │
- │ 2* 0Ah │ 6 0Eh │ 10 72h │ 14 76h │
- │ 3 0Bh │ 7 0Fh │ 11 73h │ 15 77h │
- └───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
-
- * Note IRQ2 is not usable directly except on the original PC and XT, which do
- not have IRQ8-15. The slot bus connection that was IRQ2 on the PC and XT is
- connected to IRQ9 on AT-class ISA machines. The BIOS default handler for IRQ9
- (int 71h) invokes the IRQ2 (int 0Ah) handler for backwards compatibility.
- I don't know the details of this. (*)
-
- ## 6.6 INTERRUPT FLAG, INTERRUPT ACCEPTANCE, INTERRUPT NESTING
-
- When a hardware device requests an interrupt, the PIC tells the processor that
- an interrupt is pending. The processor has an 'interrupt enable' flag in the
- Flags (F) register, which determines whether the processor will respond to the
- interrupt request from the PIC. This flag is cleared and set by the CLI and
- STI instructions (respectively) or the disable() and enable() functions or
- pseudofunctions, which execute CLI and STI instructions (respectively).
-
- If the interrupt enable flag is clear, the processor will not action the
- interrupt request. In this state, the PIC will continue to examine its
- inputs, and keep evaluating which interrupt is the highest priority active
- interrupt request, leaving its interrupt request line to the processor in
- the active state.
-
- Interrupts are prioritised, with IRQ0 (the timer tick) being highest priority
- and IRQ7 being lowest priority. IRQ8-15 fit in the gap between IRQ1 (the
- keyboard scancode interrupt) and IRQ3 (normally used for COM2). This priority
- is determined by control bytes sent to the PICs by the BIOS initialisation code.
- It can be changed by reinitialising the PICs but I know of no program that does
- this.
-
- When the processor is able to accept the interrupt request, it pushes the flags
- and the CS and IP registers onto the stack, and clears the interrupt flag in
- the flags register, before allowing the PICs to decide which is the highest
- priority interrupt and provide the address of the interrupt vector. The
- processor then executes the interrupt handler for the highest priority pending
- IRQ. The interrupt routine ends with an IRET, which is like a RETF but also
- pops the flags, i.e. 'undoes' the automatic stacking done by the processor when
- the interrupt was registered.
-
- During execution of the handler, the PICs continue to evaluate the highest
- priority interrupt being requested, and if an interrupt with a higher priority
- than the one in progress comes along, they will issue another interrupt request
- to the processor. The processor will ignore this request unless the interrupt
- handler in progress has explicitly enabled interrupts, by executing an STI or
- enable(). In this case, if a higher priority interrupt is pending, the handler
- in progress will itself be interrupted, so that the higher priority interrupt
- can be serviced. On return from the higher priority interrupt handler, the
- lower priority handler will resume.
-
- If during servicing of an interrupt, a lower priority interrupt source comes
- along, or the same interrupt is retriggered, the PIC will not interrupt the
- processor. Once the handler in progress has terminated, the lower or same
- priority interrupt will be actioned.
-
- The PIC knows which interrupt level is in progress, because it triggered the
- interrupt itself. But it cannot tell when that interrupt level has been
- processed. The interrupt handler has to tell it, via the EOI command, see
- section »» 6.28.
-
- As the timer tick has the highest priority, care should be taken to ensure that
- it is as short and efficient as possible, because it cannot be interrupted, even
- if it enables interrupts. See section »» 6.9 for an exception to this rule.
-
- ## 6.7 EMM386 INTERRUPT INTERCEPTION
-
- EMM386 places the 80x86 into virtual 8086 mode and intercepts interrupts at a
- hardware level, i.e. through specific features of the 386 and later processors.
- This is different from intercepting interrupts at the vector level. The reason
- for this behaviour is that several interrupts serve dual purposes - they are
- IBM-allocated hardware or software interrupts, but are also Intel-allocated
- processor interrupts known as processor exceptions (section »» 6 introductory).
-
- In real mode, the 80x86 will not generate these new internal interrupts, and
- behaves like an 8086/8088, 80186, or 80286, but EMM386 must put the 80x86 into
- virtual 8086 mode, so that it can use the paging facilities of the 386/486/586
- to remap memory, etc. In virtual 8086 mode, these exceptions may occur.
- However, DOS and BIOS functions are designed assuming real mode, and do not
- expect to be called when these exceptions occur, therefore EMM386 must intercept
- these interrupts and when they occur it must determine whether the interrupt is
- a real-mode interrupt (in which case it invokes the appropriate interrupt
- handler via its vector) or a processor exception (in which case it displays a
- friendly message asking whether you want to terminate the program, then usually
- locks up the machine regardless :-)
-
- The extra time required for EMM386 to determine the interrupt type adds a
- significant amount of overhead to each interrupt, as demonstrated by the
- example in section »» 6.8 (software interrupts) and the sample program in
- section »» 10.16 (hardware interrupt).
-
- If anyone has more insight into EMM386 and its effects on interrupts, please
- let me know. (*)
-
- ## 6.8 AVOIDING EMM386 OVERHEAD
-
- Because EMM386 intercepts the interrupt at the hardware level, it can be
- bypassed by calling the interrupt handler directly via its interrupt vector,
- avoiding the actual INT instruction that will be intercepted by EMM386.
- This applies to software interrupts (i.e. function interrupts for BIOS, DOS,
- EMS, mouse, etc functions) only. The EMM386 overhead on hardware interrupts
- (IRQs) cannot be bypassed.
-
- When doing this manually, care must be taken to ensure that the processor is
- in the correct state as expected by the interrupt handler. This involves
- ensuring that the interrupt flag is clear. Quite a lot of messing around
- is required for a generic solution that preserves all ingoing registers
- including the flags (apart from the interrupt flag, of course), as shown in
- the following code section, which demonstrates how to call a software interrupt
- directly thus bypassing the EMM386 overhead:
-
- -------------------------------- snip snip snip --------------------------------
- IntNum EQU 10h ; Interrupt to be invoked
-
- pushf
- sub sp,8
- push bp
- push ax
- push ds
- mov bp,sp
- push WORD PTR [bp+14] ; Take a copy of the flags
- mov [bp+12],cs ; Segment of return address
- mov WORD PTR [bp+10],OFFSET ReturnPoint ; Offset of same
- xor ax,ax
- mov ds,ax ; Address interrupt vector table
- mov ax,ds:[(IntNum SHL 2) + 2] ; Segment of handler
- mov [bp+8],ax
- mov ax,ds:[IntNum SHL 2] ; Offset of handler
- mov [bp+6],ax
- popf
- pop ds
- pop ax
- pop bp
- cli
- retf ; 'RETF' to handler
- ReturnPoint: ; Continue
- -------------------------------- snip snip snip --------------------------------
-
- This code section is rather convoluted. It sets up a stack frame as follows:
-
- BP+... Contains
-
- 14 Flags at subroutine entry
- 12 Segment of ReturnPoint
- 10 Offset of ReturnPoint
- 8 Segment of handler
- 6 Offset of handler
- 4 BP
- 2 AX
- 0 DS
- -2 Flags (copy of BP+16)
-
- A program to compare the speed of 50000 loops calling video BIOS functions 0Eh
- (teletype output) twice, 3 (request cursor position and size), and 8 (read
- character and attribute at cursor), first using an INT 10h to call the BIOS
- function and then using the above code, gave the following results on my
- 486DX2-66:
-
- EMM386 Method Time Speed (relative)
-
- Not present Int 10h 838730 100% (normalised)
- Not present Above code 991420 84.6%
- Present Int 10h 2084600 40.2%
- Present Above code 1440275 58.2%
-
- These results show that installing EMM386 slows the system significantly, but
- by calling the interrupt directly, some of the overhead is removed. The
- speed improvement gained by calling the interrupt instead of issuing an INT
- instruction, when EMM386 is installed, is 44.7%. Without EMM386 installed,
- calling the interrupt is slower than using INT, because of the messy stack
- manipulation.
-
- If anyone has any comments on these findings, please let me know. (*)
-
- ## 6.9 LONG TIMER TICK INTERRUPT HANDLERS
-
- A special method may be used if the timer tick interrupt handler must take a
- long time. Interrupts may be enabled and an EOI sent to the PIC, making the
- PIC think that the timer tick interrupt has been fully serviced. This allows
- lower priority interrupts to be serviced and handled in the normal way (but
- see section »» 6.9.1), thus preventing problems with the keyboard, mouse, and
- serial I/O, but of course this means that another timer tick interrupt could
- come along while the current handler is in progress. This will cause the int
- 8 handler to be re-entered unless the condition is detected using a flag or
- 'semaphore'. If on entry to the interrupt handler the semaphore is set, the
- interrupt handler must send an EOI and exit, or exit by chaining to the
- original handler.
-
- Here is an example timer tick interrupt intercepter that hooks into int 8 and
- implements this technique. Note that the Int8Sem and TriggerFlag variables
- appear in the code segment, to allow them to be accessed via CS (this avoids
- wasting time manipulating DS).
-
- -------------------------------- snip snip snip --------------------------------
- ASSUME cs:_TEXT ; Current code segment name
- ASSUME ds:nothing,es:nothing,ss:nothing
-
- Int8Sem DB 0 ; Int 8 in progress semaphore
- TriggerFlag DB 0 ; Flag to trigger long function
-
- NewInt08 PROC far ; Int 8 intercepter
- pushf ; Preserve flags
- cli ; Make sure interrupts are off
- cmp Int8Sem,0 ; Check whether we're already busy
- jnz GoOld08 ; If so, don't do anything
-
- ; Decide here whether the long function should be performed. If so,
- ; branch to DoLongFunc, otherwise continue. TriggerFlag must be set
- ; by some external routine in order to trigger the background function.
- ; TriggerFlag will be reset to zero by the function when it completes.
-
- cmp TriggerFlag,0 ; Time to perform the function?
- jnz DoLongFunc ; If so
-
- ; Idle - just jump to old handler
-
- GoOld08: popf ; Fix stack
- DB 0EAh ; JMP xxxx:yyyy
- Old08Ofs DW 0 ; Vector to original handler - Offset
- Old08Seg DW 0 ; Segment
-
- ; Time to perform the long function
-
- DoLongFunc: inc Int8Sem ; Set busy flag
- pushf ; Simulate stack for an INT
- call DWORD PTR Old08Ofs ; Chain to old handler (sends EOI)
- sti ; Enable interrupts
- push dx ; Preserve
- push cx ; Preserve
- push bx ; Preserve
- push ax ; Preserve
-
- ; -- Insert code here to perform your long function. Preserve any other
- ; registers that you will use, using PUSH and POP. Note the asynchronous
- ; interrupt handler restrictions still apply (this routine cannot call DOS,
- ; etc), but this code may take as long as necessary.
-
- ; -- Your code goes here
-
- pop ax ; Restore
- pop bx ; Restore
- pop cx ; Restore
- pop dx ; Restore
- mov Int8Sem,0 ; No longer busy
- mov TriggerFlag,0 ; We have triggered
- popf ; Restore flags pushed at start of int 8
- iret ; Finally, return to application
- NewInt08 ENDP
- -------------------------------- snip snip snip --------------------------------
-
- This approach can be thought of as an interrupt-triggered 'branch' to another
- section of code. Once this interrupt intercepter calls the old handler to send
- the EOI and enables interrupts, it effectively becomes the 'mainline' and runs
- 'in the foreground', itself being interrupted by other interrupts of any
- priority. After completing its function, it may return control to the point
- where the interrupt interrupted execution, or it may choose not to do so
- (though this requires careful programming). Generally the interrupt handler
- restrictions still apply, because you do not know what the machine was doing at
- the time that the interrupt occurred.
-
- If used in a TSR, this technique can cause another problem. If another program
- hooks int 8, and chains to our interrupt handler using the CALL method so it can
- regain control after chaining, that program's interrupt handler may be called
- recursively. There are no formal guidelines for writing TSRs (at least, not at
- this level of depth) so I don't know how this should be handled. Anyone? (*)
-
- There _are_ TSRs that use this technique - I believe DOS's PRINT program does
- this - and it may have implications on int 8 handlers which chain to the
- original handler using the CALL method (see section »» 6.31). (*)
-
- Manipulation of semaphores is usually done using indivisible instructions, such
- as the XCHG instruction, so that it's not possible for the code to be re-entered
- between the semaphore test, and the semaphore set. In this case, that section
- of code is uninterruptible, because interrupts are explicitly locked out, so an
- indivisible instruction is not required. The explicit CLI instruction should
- not be needed, as the interrupt flag is turned off when the processor branches
- to the interrupt service routine, but it is good practice to explicitly disable
- interrupts here, as there are some programs which intercept int 8 but call the
- original handler with interrupts enabled.
-
- ## 6.9.1 DANGER OF LONG TIMER TICK INTERRUPT HANDLERS
-
- There is one further problem with this technique. If a lower priority interrupt
- was being handled while the int 8 occurred, and is still in progress, it will
- not complete and send its EOI until the int 8 handler completes and returns.
- Therefore, that interrupt and all lower priority interrupts, are locked out
- for the duration of the extended int 8 code. In some cases, it may be useful
- to poll the interrupt controller's In Service Register at the start of the int
- 8 handler, and if any other interrupts are already being serviced, do not do
- the extended code. If the interrupt handler _must_ execute the extended code
- regardless of whether any lower priority interrupts are being serviced, this
- may have an impact on other system functions. Ideas anyone? (*)
-
- ## 6.10 INTERRUPT MASK REGISTER
-
- Hardware interrupts may be masked individually via the Interrupt Mask Registers
- (IMRs) in the 8259 PIC chips. Each PIC has an 8-bit IMR, in which each bit
- corresponds to an IRQ line. Bits 0-7 in the primary PIC's IMR correspond to
- IRQ0-7 respectively, and bits 0-7 in the secondary PIC's IMR correspond to
- IRQ8-15 respectively. If IRQ2 is masked off in the primary PIC, this masks
- off IRQ8-15, as they are signalled by the secondary PIC via this cascade input
- on the primary PIC. Therefore, when enabling any IRQ on the secondary PIC
- (i.e. IRQ8 through 15), you should also explicitly enable IRQ2 on the primary
- PIC.
-
- If the bit in the IMR is set, the interrupt is _masked_ (i.e. disabled). This
- is the opposite of the interrupt enable flag in the processor, which is set to
- _enable_ interrupts. Setting a bit in the IMR _masks_ (prevents) the interrupt.
-
- The IMR is a read/write register and can be accessed at I/O address 21h (primary
- PIC) or 0A1h (secondary PIC). See section »» 6.11 for code examples.
-
- The PIC also contains an interrupt request register (IRR) and an in-service
- register. These can be read by issuing the appropriate read-back command to
- the PIC and then reading the command/status register at I/O address 20h (primary
- PIC) or 0A0h (secondary PIC), see sections »» 6.12 and »» 6.13.
-
- ## 6.11 ENABLING AND DISABLING THE TIMER TICK INTERRUPT
-
- Interrupt 8 can be enabled or disabled via bit zero of the interrupt mask
- register (IMR) in the primary 8259 PIC, at I/O address 21h. Each bit in
- this register controls the correspondingly numbered IRQ, and int 8 is IRQ0.
- Setting the bit _masks_ or _disables_ the interrupt, thus the name 'interrupt
- _mask_ register'.
-
- Disable interrupts using disable() or CLI around accesses to the IMR. Here are
- two sample subroutines to control int 8.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- DisableInt8: ; Destroys AL only
- pushf
- cli
- in al,21h
- or al,1
- out 21h,al
- popf
- ret
-
- EnableInt8: ; Destroys AL only
- pushf
- cli
- in al,21h
- and al,0FEh
- out 21h,al
- popf
- ret
- -------------------------------- snip snip snip --------------------------------
-
- ## 6.12 READING THE INTERRUPT REQUEST REGISTER
-
- The IRR in the primary 8259 PIC can be read with the following code fragment.
- It returns the IRR value in AL.
-
- -------------------------------- snip snip snip --------------------------------
- ReadPIC0IRR: ; Returns IRR in AL
- pushf
- cli
- mov al,0Ah ; Read IRR command
- out 20h,al
- jmp SHORT $+2
- in al,20h ; Read the IRR value
- popf
- ret
- -------------------------------- snip snip snip --------------------------------
-
- If you know that no other software is going to be accessing the PIC, for example
- if you are reading the IRR in a loop with interrupts locked out, you can skip
- sending the 0Ah to port 20h before every read of the IRR. The PIC remembers
- that the IRR is selected to appear on a read of port 20h. So you would send the
- 0Ah to port 20h at the start of the loop, then just read port 20h every time
- through the loop.
-
- The same routine can be adapted to read the secondary PIC, just access port 0A0h
- instead of port 20h.
-
- ## 6.13 READING THE INTERRUPT IN SERVICE REGISTER
-
- The ISR (In Service Register, not Interrupt Service Routine) in the primary 8259
- PIC can be read with the following code fragment. It returns the In Service
- Register value in AL.
-
- -------------------------------- snip snip snip --------------------------------
- ReadPIC0ISR: ; Returns ISR in AL
- pushf
- cli
- mov al,0Bh ; Read ISR command
- out 20h,al
- jmp SHORT $+2
- in al,20h ; Read the ISR value
- popf
- ret
- -------------------------------- snip snip snip --------------------------------
-
- The In Service Register tells you what other interrupts are 'in service'. For
- example, if a serial port interrupt occurred on IRQ4, and during processing of
- that interrupt (before the EOI was sent to the PIC), a keyboard interrupt on
- IRQ1 occurred, and again during processing of that interrupt, the timer tick
- interrupt came along, then the In Service Register would contain 00010011
- binary if read inside the timer tick interrupt, indicating that IRQ4, IRQ1,
- and IRQ0 are currently 'in service', i.e. their handlers are in progress and
- are nested.
-
- If in the above example, the IRQ4 handler completed and sent its EOI to the
- PIC before the IRQ1 occurred, its bit would not be set in the In Service
- Register.
-
- Because only higher priority (lower-numbered) interrupts can interrupt an
- interrupt handler (unless it sends an EOI early, in which case it is no longer
- "in service"), any interrupts flagged in the In Service Register must have
- occurred one after the other in order, from highest IRQ number to lowest IRQ
- number.
-
- The same routine can be adapted to read the secondary PIC, just access port 0A0h
- instead of port 20h.
-
- ## 6.14 WHEN YOU SHOULD DISABLE INTERRUPTS
-
- Generally, your program should disable interrupts around a sequence of accesses
- to an I/O device (such as the PIC, CTC, VGA chips, etc) to ensure that an
- interrupt service routine does not come along during your access sequence and
- access the chip, disrupting your access sequence. Interrupts should also be
- locked out when reading or writing variables that may be being accessed by an
- interrupt routine, unless you carefully design your communication with the
- interrupt routine so that this is not necessary.
-
- In particular, accesses to peripherals such as the RTC and CRTC (CRT controller)
- which have an address register and a data register, must be made with interrupts
- disabled, as if interrupts are enabled during such accesses, an interrupt
- handler could access the device and change the address register after your code
- had set the address but before your code had accessed the register, causing
- your code to access the wrong register, with possibly disastrous results - e.g.
- an ex monitor :-)
-
- If this results in interrupts being locked out for less than ten microseconds
- at a time, this will be acceptable for all normal applications. It might not
- be acceptable if the timer tick is running very much faster than usual and low
- jitter is needed - see section »» 6.15.
-
- Even when no address register is involved, interrupts should still be locked
- out over access sequences. For example, with interrupts enabled, the sequence
-
- in al,21h
- or al,1
- out 21h,al
-
- looks innocent enough, but what happens if an interrupt is triggered between
- the IN and the OUT, and the interrupt routine also modifies the IMR, to turn
- on, or turn off, an unrelated interrupt? The interrupt handler will do its
- thing, but as soon as it returns, the IMR will be clobbered by an old copy of
- the IMR with bit 0 set, breaking the changes made by the interrupt handler.
-
- ## 6.15 WHEN YOU SHOULDN'T DISABLE INTERRUPTS
-
- These guidelines apply to DOS and any similar single-tasking operating system.
-
- The maximum length of time that interrupts can safely be locked out for depends
- on the operating environment. Although there are no formal guidelines, I would
- suggest 100 microseconds as a reasonable limit for good performance, and a few
- milliseconds as a sensible upper limit. If high speed timer tick interrupts or
- high speed serial communication are being used, the limit will typically be much
- lower, depending on the required interrupt rate, to avoid missing interrupts
- altogether.
-
- If you require accurately timed interrupt delivery, beware that disabling
- interrupts for even a short length of time will cause 'interrupt delivery
- jitter' (thanks {JAM} for the term :-) - i.e. occasionally the interrupt will
- be delayed slightly. See section »» 6.16 for details.
-
- Locking interrupts out for more than 50 ms continuously may cause missed timer
- ticks and problems with the keyboard and network (if present), at least.
-
- If a fast timer tick interrupt (see section »» 8 and subsections) is being used,
- or another demanding high speed interrupt such as high speed serial reception,
- it is easier to miss an interrupt (or lose data). If a hardware interrupt is
- missed because interrupts are locked out, the PIC does not generate an extra
- interrupt.
-
- ## 6.16 CAUSES OF INTERRUPT DELIVERY JITTER AND FAST TICK LOSS
-
- Interrupt delivery jitter ({JAM}'s term) occurs when interrupt acceptance is
- delayed, i.e. there is an unusual or inconsistent delay between the interrupt
- being signalled at the hardware level, and the processor starting execution of
- the interrupt handler, and the interrupt is serviced late. This happens for
- three reasons:
-
- ■ Interrupts are locked out (processor's interrupt flag is clear)
- ■ Equal or higher priority interrupt in progress (see section »» 6.6)
- (not normally applicable to the timer tick interrupt)
- ■ Instruction or DRAM refresh in progress (contributes a very small
- amount of jitter, and are unavoidable)
-
- The first reason is the usual reason for interrupt delivery jitter on the timer
- tick interrupt (int 8 and int 1Ch). The first and second reasons are the usual
- cause of interrupt delivery jitter on other interrupt sources.
-
- For the normal 18.2065 Hz timer tick interrupt, this causes the delivery of
- interrupts to be uneven (i.e. to jitter slightly), in either a random or a
- partly random, partly cyclic manner. This is not usually a problem, as the
- low resolution timer tick is not used when timing requirements are critical.
-
- Interrupt jitter can also affect cases where the timer interrupt itself is not
- delayed; for example if an absolute timestamp (see section »» 9) is being used
- to timestamp serial data received under interrupt or some other occurrence that
- is signalled via an interrupt, if that interrupt is delayed, the timestamp will
- reflect the time that the interrupt was serviced, rather than when it was
- signalled by the serial chip.
-
- If an interrupt must be actioned within a short length of time, for example a
- serial received character interrupt or a fast tick interrupt (used when the
- tick rate is increased), delayed interrupt acceptance may result in a missed
- interrupt. If this occurs, the PIC does not generate an extra interrupt, in
- other words, the whole interrupt is lost, and this results in a cumulative
- timekeeping error unless the condition is detected and handled specially (see
- section »» 6.17).
-
- There are three main causes of interrupt delivery jitter due to interrupts
- being locked out:
-
- ■ Real (hardware) interrupts
- ■ Software interrupts
- ■ Interrupts disabled while accessing hardware or volatile variables
-
- These causes are now described individually.
-
- ## 6.16.1 INTERRUPT DELIVERY JITTER DUE TO REAL INTERRUPTS
-
- Real interrupts include the timer tick (int 8), keyboard scancode (int 9),
- serial communication (if enabled) (including serial and bus mouse), RTC (int
- 70h) (if enabled), and network card (if present) interrupts. The handlers for
- all of these interrupts (except the timer tick) should re-enable interrupts
- quickly so that higher priority interrupts including the timer tick are not
- delayed for long, but some handlers do not enable interrupts because of bad
- design or deliberately, due to other considerations. Also EMM386 imposes extra
- overhead during interrupt acceptance; during this time another interrupt cannot
- be accepted, I think. (*)
-
- For example, if a network card interrupt handler on IRQ3 (for example) does not
- enable interrupts, and this interrupt is invoked on every network data block
- received by the machine, then every time a block of data is received, interrupts
- are locked out for, perhaps, 100 us. If a timer tick interrupt is signalled
- during this time, its acceptance will be delayed for up to 100 us, and interrupt
- delivery jitter occurs.
-
- Also, {JAM} points out that screen savers, which typically hook int 8, often
- clear the whole screen with interrupts disabled, resulting in a very long
- int 8 every once in a while when the screen saver 'kicks in'. Many other
- programs also intercept int 8 and will increase the amount of time occupied
- by each int 8 and/or by occasional int 8 invocations - network software uses
- int 8 as a timebase for timeout detection {JAM}, mouse drivers use it, and
- lots of pop-up and non-pop-up TSRs also use int 8.
-
- {TOR} points out that some BIOSes can also be the culprit:
-
- > The usual BIOS implementation of the keyboard interrupt and the floppy drive
- > interface are among the worst for blocking interrupts. I have actually
- > seen a keyboard driver [int 9 handler] issue its buffer full 'beep' with
- > interrupts locked out...
-
- ## 6.16.2 INTERRUPT DELIVERY JITTER DUE TO SOFTWARE INTERRUPTS
-
- Every time a software interrupt is issued, the processor disables interrupts
- before executing the interrupt handler. Well-behaved software interrupt
- handlers re-enable interrupts immediately on entry, but not all software
- interrupt handlers are well-behaved. In any case, there is a short length of
- time during which interrupts are disabled, and this time is lengthened if
- EMM386 is installed, because EMM386 intercepts the interrupt at a hardware
- level, and has to work out whether the interrupt is software-generated or is
- processor-generated, because several low-numbered interrupts are both processor
- exceptions and software interrupts.
-
- Therefore, every time your program or any code called by your program issues a
- software interrupt, interrupts are locked out for a short time, and possibly a
- long time if the interrupt handler is badly written or if several programs
- (e.g. TSRs) have intercepted that interrupt and many interrupt chains are
- performed before the request reaches its actual handler.
-
- Other software interrupts which may spend a significant length of time with
- interrupts locked out are:
-
- ■ Screen scrolling via the BIOS
- ■ Hard drive read, write, and seek accesses (possibly)
- ■ Network accesses
- ■ Mouse driver function calls
- ■ EMS function calls
- ■ XMS function calls
- ■ Potentially, any code you did not write yourself!
-
- ## 6.16.3 INTERRUPT DELIVERY JITTER DUE TO HARDWARE ACCESSES
-
- Often, interrupts must be disabled manually, using CLI, around access sequences
- to hardware devices (see section »» 6.14) or accesses to volatile variables
- that may be modified by a hardware interrupt handler. If an interrupt is
- flagged during the short time that interrupts are locked out, it will be
- delayed until interrupts are re-enabled, causing interrupt delivery jitter.
-
- There are also other reasons why software might disable interrupts, usually
- (but not always) for short periods only. If your program requires very low
- jitter, it will probably have to do everything itself, because it cannot call
- any normal BIOS or DOS functions!
-
- ## 6.16.4 AVOIDING INTERRUPT DELIVERY JITTER
-
- If your application must run with a very fast timer tick interrupt, or must
- have very low interrupt jitter for whatever reason, it must avoid all of the
- causes of interrupt jitter described in sections »» 6.16 through »» 6.16.3.
-
- Interrupt jitter due to instruction execution (i.e. the interrupt cannot be
- accepted until the instruction in progress is completed) is unavoidable, but
- could probably be reduced by using short instructions and avoiding prefixes.
- Other causes of interrupt delivery jitter must be avoided for good results.
-
- This comes down to the following restrictions:
-
- ■ Disable all hardware interrupt sources via the PIC(s)
- except the interrupt source you are using
- ■ Do not issue software interrupts
- ■ Do not call code over which you do not have control
- ■ Do not chain to the original interrupt handler
- ■ Do not disable interrupts using CLI at all
- ■ Run the program without EMM386 if possible
-
- Following these guidelines should ensure that interrupts are never locked out
- due to a hardware interrupt, software interrupt, or deliberate execution of a
- CLI instruction.
-
- Disabling IRQ1 (keyboard scancode) will disable the keyboard, of course, and
- disabling serial interrupts may disable the mouse. Most other interrupts are
- not active in the background (e.g. the floppy disk interrupt is only active
- when a disk access is in progress) and should be unaffected.
-
- If you are using int 8 and the interrupt rate is fairly slow, you may choose to
- chain to the original int 8 handler, because this will not cause jitter as it
- only executes _after_ the interrupt has been registered. However, this could
- cause problems if TSRs and/or drivers are using int 8 and occasionally do
- something nasty such as using the long tick interrupt handler technique
- described in section »» 6.9, where they gain full control of the machine for
- a while. In short, if you chain to the original handler, you are giving
- execution to code over which you have no control, so if jitter is critical,
- you do so at your own risk!
-
- If you need very fast interrupts or very low interrupt jitter, be very careful
- about what you do and who you call - you may need to do everything yourself to
- avoid interrupt latencies!
-
- I don't know the details of EMM386 and its effects on interrupt jitter.
- For example, it may internally trap some privileged instructions, and delay
- interrupts while processing these instructions. If anyone knows the details,
- please let me know! (*)
-
- ## 6.17 DETECTING INTERRUPT DELIVERY JITTER AND MISSED FAST TICK INTERRUPTS
-
- Interrupt delivery jitter on int 8 can be detected by reading CTC channel zero
- on entry to your interrupt handler and looking at the amount of variation from
- the highest raw value read, or the expected raw value (assuming that the reload
- value is known). See the sample program in section »» 10.16.2 for an example
- of this technique. If your application will be sensitive to interrupt jitter,
- you should incorporate this type of check, and if jitter is excessive, perhaps
- advise the user that there is a problem and he/she should ascertain which driver
- or TSR is causing the problem and get technical help to fix it if possible.
-
- If a fast timer tick rate is being used, a missed interrupt can be detected by
- using another CTC channel as a reference, providing that the CTC channel is not
- required (and will not be touched) for any other purpose. I would suggest using
- channel two, which is normally used for speaker audio generation. You would set
- channel two to a large divisor (e.g. 65536) and mode two, and make sure that
- nothing else touched it - i.e. disable, or at least don't use, the BIOS video
- function that emits a beep (int 10h with AX = 0E07h), and possibly hook into the
- keyboard subsystem to prevent the beep when the type-ahead buffer gets full.
-
- Your fast tick interrupt routine would read a timestamp from CTC channel two
- to determine how many fast ticks have been missed and adjust its behaviour
- accordingly. This approach would prevent the cumulative error, but would not
- fix the 'jumpiness' or 'jitter' of the timekeeping.
-
- ## 6.18 DISABLING INTERRUPTS FOR LONGER THAN ONE TIMER TICK
-
- In some applications, you may choose to disable interrupts for longer than the
- recommended maximums in section »» 6.15. You can also selectively disable the
- timer tick interrupt and any other hardware interrupt source, via the PIC IMR
- (section »» 6.11). You will have to deal with the implications of doing this,
- however.
-
- While interrupts are disabled via the processor's interrupt flag, interrupts
- accumulate, so as soon as the interrupt flag is set (via a POPF or STI), {JAM}
- says: "the program does not regain control until ALL outstanding interrupts are
- processed, including interrupts that happen while the outstanding ones are being
- handled. On networked machines, that time may be in milliseconds!"
-
- ## 6.19 DISABLING INTERRUPTS FOR LONG PERIODS OF TIME
-
- If it is necessary to disable interrupts for a long period of time, causing
- timer ticks to be missed, be aware that doing this is likely to sabotage any
- network software on the machine, and will also break the mouse driver while
- interrupts are locked out. You should take the following precautions.
-
- Don't start the section of code where interrupts are locked out, until the
- floppy disk drive motors have all turned off. Check the byte at low memory
- location 0040:003F. If it's nonzero, one or more floppy disk drive motors
- are active. Wait until it is zero.
-
- Assuming you don't want the machine to lose time, you can either read CTC
- channel zero regularly and watch for a borrow and increment the BIOS timer
- tick count when that occurs (remember the wrap-around and the midnight flag),
- or upon completion of the no-interrupt section of code, read the RTC and
- calculate and store the appropriate timer tick value. This also requires
- setting the midnight flag if appropriate.
-
- Generally if you want to disable interrupts for _that_ long, you will be
- running the program on a dedicated machine, and you may not be too concerned
- about loss of time. In this case, since you have control of the machine that
- the software will be running on, you could install the ATRTC driver, see
- section »» 3.3, which removes the dependency on the BIOS timer tick for
- timekeeping. The other problems still remain, however.
-
- ## 6.20 OVERHEAD OF AN INTERRUPT
-
- When an interrupt is accepted, the processor branches to the interrupt handler.
- On modern processors, this causes the prefetch queue to be flushed, wasting a
- small amount of time. Of course the prefetch queue is being flushed all the
- time, by branches and jumps and calls, etc, so this is not a major problem.
- The prefetch queue will be flushed when the interrupt is accepted, and again
- when the interrupt handler returns with an IRET.
-
- A bigger problem is code and data caching. {JAM} Because this caching is done
- in blocks, the interrupt may cause wanted code to be flushed from the cache,
- to make room in the cache for the interrupt handler code, wasting considerable
- time in reloading the cache when the interrupt completes.
-
- There is nothing that can be done about either of these problems.
-
- {JAM} In protected mode, interrupt overhead is very much higher, because of
- the privilege changes, mode switches, etc that are involved. Interrupt
- overhead on a 386SX-25 is in the order of a few hundred microseconds.
-
- I assume this refers to a real-mode interrupt handler being used with protected
- mode code. If the interrupt handler operated in protected mode, or was a dual-
- mode interrupt handler (could operate in either mode), this overhead would not
- exist, presumably. If anyone has more detailed information on this subject,
- please let me know. Also any detailed information on what EMM386 does to
- interrupts and how much overhead it imposes, and if there is any way to bypass
- it, would be great. (*)
-
- Tor Sjowall {TOR} also mentions an additional source of interrupt overhead -
- the stack switch that DOS does on hardware interrupts if you have a line
- 'STACKS=X,Y' in CONFIG.SYS. I don't know how this stack switching works, or
- at what level it operates. Please let me know if you can help. (*)
-
- ## 6.21 EFFECT OF BACKGROUND INTERRUPTS
-
- The timer tick interrupt is normally permanently enabled, and from the point of
- view of the code being interrupted, it introduces a 'gap' in time, at regular
- intervals (assuming interrupts are enabled). You could imagine that the
- processor gets abducted by aliens in a UFO (if you had a vivid imagination :-)
- One moment it's executing your main routine, minding its own business, then
- suddenly it is taken away and made to do something else, then when it returns
- to where it was, it continues normally, without even knowing that it had been
- doing something else, except that some time has elapsed. Excuse the analogy.
- Your foreground code is constantly being interrupted without its knowledge.
-
- {JAM} explains this quite nicely as follows:
-
- "The IBM PC has a constant active background process that results in a small gap
- in any loop. This becomes magnified when programs are compiled for protected
- mode. Moreover, the standard hardware can add additional gaps. Most often
- these gaps are under our control. Finally, when connected to a network, many
- types of background activities can happen, most of which we cannot predict and
- are beyond our control. Whenever we design a program to function on networked
- machines, we must remember that these background processes are in effect and we
- must take them into account. For example, when we poll a device, we must be
- aware that there will be missing time slices from that polling".
-
- Unless specifically enabled by your program, the only interrupt sources likely
- to be operating regularly while the machine is idle, are int 8 (timer tick),
- int 9 (keyboard scancode), and interrupts for the serial mouse or bus mouse,
- and network card interrupts.
-
- ## 6.22 SAFE CONTROL OF INTERRUPTS
-
- When you access hardware devices (reading or writing the CTC registers, for
- example), you could disable interrupts around the access, like this:
-
- -------------------------------- snip snip snip --------------------------------
- void write_some_registers(void) { /* Unsafe method! */
- disable(); /* Or asm cli */
- outportb(port1, value1); /* whatever you need to do */
- outportb(port2, value2); /* whatever you need to do */
- enable(); /* Or asm sti */
- return;
- }
- -------------------------------- snip snip snip --------------------------------
-
- This assumes that the function that called this function was operating with
- interrupts enabled, and wants them re-enabled when this function has finished
- talking to the hardware. This may not be the case!
-
- For example, the function that called this function may already be doing
- something critical which requires interrupts to be locked out, and remain
- locked out continuously during the call to our write_some_registers() function.
-
- The safe way to handle this is as follows:
-
- -------------------------------- snip snip snip --------------------------------
- void write_some_registers_safely(void) {
- asm pushf;
- asm cli; /* or use disable() */
- outportb(port1, value1); /* whatever you need to do */
- outportb(port2, value2); /* whatever you need to do */
- asm popf;
- return;
- }
- -------------------------------- snip snip snip --------------------------------
-
- Here, we push the flags register onto the stack before disabling interrupts,
- then pop the flags register back once we have finished. This ensures that
- interrupts are locked out during our hardware manipulation, and also that
- the correct state of the interrupt flag is restored once we have finished.
-
- If interrupts were enabled on entry, the popf sets the interrupt flag ON, and
- the function effectively only disables interrupts for the minimum time, i.e.
- between the disable() (CLI) and the popf.
-
- If interrupts were disabled on entry, the popf sets the interrupt flag OFF,
- which it already was from the disable() (CLI instruction). Thus the routine
- _never_ enables interrupts.
-
- This simple technique ensures that the routine can be safely used in either
- situation - either interrupts allowed, or interrupts not allowed.
-
- According to an article by James Ralph (jim@grc.com) in PC Magazine, September
- 13 1994, page 340, there is a bug in some 286 processors which causes the popf
- instruction to briefly enable interrupts. The workaround proposed by James is
- to use an IRET (which presumably does not suffer from this bug) instead of a
- POPF (an IRET pops IP, CS, and the flags). This approach requires that you push
- CS and IP onto the stack first. The example given by James is similar to this:
-
- -------------------------------- snip snip snip --------------------------------
- pushf ; Keep flags including interrupt flag
- cli ; Disable interrupts
-
- ; Do critical stuff in here - interrupts are locked out
-
- push cs ; Have flags on stack, now push CS
- call NEAR AnIRET ; CALL pushes IP, IRET pops IP, CS, flags
-
- ; Continue with the main function - interrupt flag is now restored to its
- ; original value on entry to the function
-
- ret ; End of the function
-
- ; Put the IRET somewhere in the code segment - it can be used by multiple
- ; instances of the above code.
-
- AnIRET: iret
- -------------------------------- snip snip snip --------------------------------
-
- Another way to handle this would be:
-
- -------------------------------- snip snip snip --------------------------------
- pushf ; Keep flags including interrupt flag
- cli ; Disable interrupts
-
- ; Do critical stuff in here - interrupts are locked out
-
- push cs ; Have flags on stack, now push CS
- push WORD PTR cs:RetAdr ; Push a value for IP
- iret ; Pops IP, CS, and flags
- RetAdr DW RetPoint ; Offset to 'return' to
- RetPoint:
-
- ; Continue with the main function - interrupt flag is now restored to its
- ; original value on entry to the function
-
- -------------------------------- snip snip snip --------------------------------
-
- I have not taken this precaution in the sample code, because I'm lazy, but you
- probably should use this method unless your program will never be run on 286
- machines or is 386/486/586-specific.
-
- ## 6.23 TIMER TICK INTERRUPT HANDLER GUIDELINES
-
- Note that these comments also apply to other asynchronous interrupts, such as
- the keyboard interrupt (int 9) and the serial and parallel port interrupts.
- For full details, find a DOS reference that discusses ISR programming and TSR
- techniques.
-
- Both int 8 and int 1Ch are asynchronous hardware-triggered interrupts (although
- int 1Ch is actually software-generated). See section »» 6.35 for a discussion
- of the differences in usage between int 8 and int 1Ch.
-
- There are major restrictions on what can safely be done inside an asynchronous
- interrupt handler, because when it is invoked, the hardware and software state
- of the machine is not known.
-
- For example, DOS may be in the middle of writing to a printer port, waiting for
- user input, or processing a disk I/O request, or the BIOS may be busy scrolling
- the text screen, plotting a pixel, beeping the speaker, or programming the DMA
- controller ready to transfer a sector of data from a floppy disk. Also, a C
- library function that uses static variables may be in progress. In fact, more
- than one of these 'levels' may be busy. For example, an fopen() call could be
- in progress, which called DOS, which called the BIOS to read a sector from a
- floppy disk, which is busy programming the DMA controller. Therefore, all of
- these software and hardware blocks are busy.
-
- In general it is best to limit the functions performed by an interrupt handler
- to minimal hardware manipulation, and use a shared variable interface with the
- main program, whenever possible. Be careful to make your main program aware of
- the interrupt routine - programs do not normally expect certain variables to
- change magically of their own accord. Use the 'volatile' keyword when declaring
- these variables and use disable() and enable() (CLI and STI) around any critical
- code sections.
-
- Also, if your timer tick interrupt handler may use a lot of stack space you
- should consider switching to another stack. This is much easier if the
- interrupt handler is written in assembler.
-
- Keep int 8 and int 1Ch routines as short and fast as possible to reduce delays
- imposed on other interrupt sources.
-
- ## 6.24 ACCESSING HARDWARE DEVICES IN AN INTERRUPT HANDLER
-
- Asynchronous interrupts are 'background' processes. It is not always safe for
- them to access hardware devices, because the 'foreground' processes - the main
- body of your program, or a function (e.g. DOS, BIOS, EMS, XMS, mouse, network,
- etc) called by your program - may be in the process of accessing the device, or
- may be expecting the device to remain in a certain state.
-
- Often, foreground accesses consist of reading and/or writing a few I/O locations
- in sequence, as with the CTC. To make things safe for your interrupt routines
- and for TSRs, when you access devices in this way in your foreground code, you
- must _ALWAYS_ disable interrupts around the sequence. This applies to devices
- such as the RTC, CTC, PICs, DMA controller, VGA ASICs, etc.
-
- Many hardware devices can be accessed (carefully) by an interrupt routine. This
- may be because they are not normally accessed in the foreground, or because the
- interrupt routine uses a part of the device that is not used by the foreground
- processes, or because the interrupt routine's accesses do not conflict with the
- foreground process's accesses.
-
- If you know enough to access the hardware directly, you will know when and how
- the foreground processes will access the device, so you can figure out what your
- interrupt routine can and cannot do safely with that device. Reading the CTC
- in an interrupt handler is always safe, providing that your foreground program
- is well-behaved (always disables interrupts around access sequences) and always
- reads the appropriate number of bytes from the data register, so the lobyte/
- hibyte flag remains in sync (see section »» 7.17).
-
- ## 6.25 CALLING DOS AND BIOS IN AN INTERRUPT HANDLER
-
- Much of the BIOS, and all of DOS, is not re-entrant, and therefore cannot safely
- be called from an asynchronous (hardware) interrupt handler, because it might
- have been busy (i.e. in progress) when the interrupt occurred.
-
- None of the applications in this document require DOS or BIOS to be called from
- an asynchronous interrupt handler. If you need to do this, get a reference on
- TSR programming, as it is non-trivial!
-
- ## 6.26 CALLING C LIBRARY FUNCTIONS IN AN INTERRUPT HANDLER
-
- Many C library functions call DOS or BIOS functions, and are subject to the same
- restrictions. Also, some C library functions may not be re-entrant for other
- reasons - for instance, they may use global or static variables, or allocate
- memory which is in the process of being allocated to the foreground program.
- Check your compiler's library reference or programmers' guide for information
- about TSR considerations and re-entrancy of library functions.
-
- ## 6.27 RE-ENTRY OF INTERRUPT HANDLERS
-
- Generally hardware interrupt handlers are not re-entered, i.e. are not restarted
- during their execution, because they do not send an EOI (see section »» 6.28)
- until they have completed, and then interrupts are locked out (see section
- »» 6.29). There is one exception to this rule, which applies when a TSR uses
- the long timer interrupt technique described in section »» 6.9. This technique
- can also be used with the keyboard scancode interrupt, when a TSR pops up using
- that interrupt, but see section »» 6.9.1 for a potential problem.
-
- In these cases, the interrupt handler of a foreground program may actually be
- re-entered during processing, if it chains to the original handler using the
- CALL method (see section »» 6.31), because the original handler (which is the
- TSR's handler) can issue an EOI, allowing the entry part of the interrupt
- handler to be re-entered. The TSR's own interrupt handler will be aware of
- re-entry considerations, because the TSR will be causing them, but an interrupt
- handler in a foreground program may not have been designed with this in mind.
- They probably should be designed to support this technique. See section »» 6.9
- for more details.
-
- If the code in the interrupt handler is inherently non-reentrant, this can be
- handled using a semaphore to detect re-entrance, as described in section »» 6.9.
- If the semaphore is set at the start of your handler, it should probably chain
- to the original handler using the JMP method without performing its normal
- function. In some cases it is possible that the semaphore would become set and
- would never clear. Hopefully nobody is even reading this stuff, as it is
- excessively boring. Yibble yibble yobble yoo, I am a fence post. It is time
- for my pill - I have to take one every 54.9254 milliseconds.
-
- ## 6.28 THE 'END OF INTERRUPT' SIGNAL
-
- Interrupts 8 to 15 (corresponding to IRQ0 to 7) and interrupts 70 hex to 77 hex
- (corresponding to IRQ8 to 15) are generated by hardware devices. An interrupt
- service routine for these interrupts must inform the 8259 PIC(s) when the device
- which generated the interrupt has been serviced, so that the PIC can reset its
- priority structure. This is done using a non-specific EOI (end of interrupt)
- command to the PIC. For int 8 to 15 (IRQ0 to IRQ7), a single EOI is used:
-
- outportb(0x20, 0x20); in C, or
-
- mov al,20h
- out 20h,al in assembler.
-
- For int 70 hex to 77 hex (IRQ8 to IRQ15), two EOIs are used:
-
- outportb(0xA0, 0x20);
- outportb(0x20, 0x20); in C, or
-
- mov al,20h
- out 0A0h,al
- out 20h,al in assembler.
-
- For IRQ8 through IRQ15, the EOI is typically sent to the secondary PIC first,
- as in these examples, though I don't believe there is any significance to the
- order in which they are sent.
-
- Normally the EOI is sent at the end of the interrupt routine just before the
- IRET instruction. See section »» 6.29 for interrupt control details.
-
- You can use the specific EOI command if you prefer - the value is 60 hex plus
- the IRQ number within the PIC, for example to send a specific EOI for IRQ4:
-
- outportb(0x20, 0x64);
-
- and to send a specific EOI for IRQ11:
-
- outportb(0xA0, 0x63); /* IRQ11 is input 3 on the second PIC */
- outportb(0x20, 0x62); /* The chain IRQ is IRQ2 */
-
- ## 6.28.1 LEVEL TRIGGERED INTERRUPT RESET
-
- IBM PS/2 machines that use MCA (Microchannel Architecture) buses have level
- triggered interrupts. This poses a problem for the timer interrupt - how to
- clear the timer interrupt request. I have no formal documentation on this, but
- I saw the following note in an article by Bob Smith (bobs@access.digex.net) in
- mid November 1995:
-
- > On an IBM Micro Channel Architecture system, the timer tick handler in the
- > BIOS sets the Clear IRQ0 bit (bit 7 in I/O port 61h). Without this, the
- > hard disk won't work. This might, in fact, apply to all level-triggered
- > interrupts in general, but I found out about setting that bit before having
- > to experiment any further.
-
- So it appears that the timer interrupt must be explicitly acknowledged and
- cleared on an MCA system, in addition to sending the EOI. This makes sense
- as there is no other way for the level to be reset to deassert the interrupt
- request in a level triggered interrupt system. There may be some similar
- requirement for an EISA system running in level triggered interrupt mode.
- Any more information would be welcomed. (*)
-
- This also has implications when int 8 is operated at a higher rate, because the
- int 8 intercepter would have to manually acknowledge the interrupt, in addition
- to sending the EOI, every time it didn't chain to the original int 8 handler.
- This may mean that a standard int 8 handler for a fast timer tick interrupt
- (see section »» 8) will not work on a PS/2.
-
- ## 6.29 ENABLING AND DISABLING INTERRUPTS IN AN INTERRUPT HANDLER
-
- On entry to an interrupt handler, processor interrupts are disabled (as if a
- disable() or CLI had been issued). Normal practice is to enable interrupts as
- soon as possible, perform processing, disable interrupts again, issue an EOI
- if applicable (see section »» 6.28), and return from interrupt. However, int
- 8 is the highest priority interrupt source, and until the EOI is sent, no other
- interrupts will get through (except NMI of course) so there's no need to enable
- interrupts during int 8 or int 1Ch processing, unless you hare re-ordered the
- interrupt priorities.
-
- The EOI command is sent to the PIC(s) at the end of the interrupt handler. For
- interrupt handlers which enable interrupts during processing, it is normally
- wise to disable interrupts using disable() or CLI just before issuing the EOI,
- so that another equal or lower priority interrupt does not occur after the EOI
- but before the IRET. Typical coding would be:
-
- IntHandler: sti
- push ax
- push other registers
- ; ... interrupt processing here
- pop other registers
- IntFinished: mov al,20h
- cli
- out 20h,al
- pop ax
- iret
-
- This particular consideration does not normally apply to int 8 handlers as they
- are normally the highest priority interrupt and do not need to enable
- interrupts during their operation. If an int 8 handler does enable interrupts,
- however, the above precaution should be taken.
-
- ## 6.30 STACK USAGE AND STACK CHECKING IN AN INTERRUPT HANDLER
-
- Stack usage (function nesting depth) must be kept to an absolute minimum unless
- your interrupt handler performs a stack switch to a local stack. Normally, you
- will be using the stack of whichever program was active at the moment that the
- timer tick occurred, and you don't know how much spare room there is in that
- stack.
-
- For interrupt handlers written in C, don't go allocating automatic strings or
- arrays! Declare any local variables static if possible. If your compiler has
- stack checking ON by default, and isn't too bright, you may need to turn stack
- checking OFF for all interrupt handlers, and for any functions that may be
- called by them, using the appropriate compiler directive.
-
- The directives for Borland C++ 4.0 (and probably 3.1 as well) are:
- #pragma option -N- turn OFF stack checking
- #pragma option -N turn ON stack checking if perviously enabled
- These can be placed around the whole function (or group of functions) that
- are to be compiled without stack checking, or just around the first line of
- the function (that gives the return type, function name, and parameters).
- That information was kindly sent by Michael Mauch (mauch@uni-duisburg.de) who
- mentions another problem he found with BC++ 4.0. He had an _inlined_ function
- that was called by an interrupt handler. Both the interrupt handler and the
- inlined function were declared with stack checking off. When he temporarily
- disabled inlining, during debugging, the compiler generated a stack check in
- the called function! The moral of the story is you can't always trust your
- compiler :-)
-
- If anyone can provide details of stack checking directives for other compilers,
- please let me know. (*)
-
- ## 6.31 CHAINING TO THE OLD INTERRUPT HANDLER
-
- Most interrupts have a default handler. Before your program takes over control
- of an interrupt, it must store the contents of the interrupt vector, which will
- be a far (i.e. segment and offset) pointer to the original handler, and which
- must be restored when your program terminates (see section »» 6.3).
-
- Often your replacement interrupt handler will need to use the original handler.
- This is called _chaining_ to the original handler of the interrupt, and is done
- through the pointer that your program stored when it intercepted the interrupt.
- Sometimes your replacement interrupt handler will always chain to the original
- handler, and sometimes chaining is done conditionally, i.e. when required or
- when appropriate.
-
- When chaining to an original interrupt handler, remember that the original
- interrupt handler was written to assume that it _was_ the handler for this
- interrupt source. Sometimes this requires a little care to make sure that it
- will operate properly if called by your replacement handler. For example,
- the BIOS int 8 handler issues an EOI command (see section »» 6.28) every time
- it is called, so if your interrupt handler chains to the BIOS's interrupt
- handler, it should not issue the EOI itself. It must issue the EOI if it
- does _not_ chain to the BIOS's interrupt handler, however. Also, the original
- interrupt handler will probably assume that interrupts will be disabled when
- it is invoked, as this is the case when it is invoked directly, so you must
- ensure that interrupts are disabled before chaining.
-
- There are two ways of chaining to the old handler - you can bury her, burn her
- or dump her. I mean, you can call it, or you can jump to it. Call it when
- your interrupt handler needs to regain control after the old handler has been
- invoked. Jump to it when you do not need to get control back, as this uses
- less stack space and is tidier. In the Thames.
-
- Remember that the processor pushes the flags, CS, and IP (in that order) when
- it accepts an interrupt, and an IRET (which is the way most handlers will exit)
- pops these registers back again. Therefore if you chain to a handler with a
- CALL, you must push some flags first, then use the far form of CALL, so that
- the IRET will return correctly.
-
- Chain_Call:
- ; ... Initial interrupt processing here
- pushf ; Simulate stack for an INT
- cli
- call FAR OldIntPtr ; Call old handler
- ; ... More interrupt processing here
- iret
-
- Chain_Jump:
- ; ... Initial interrupt processing here
- cli
- jmp FAR OldIntPtr ; Call old handler
-
- Note that some interrupts, specifically int 1Ch, do not require chaining, as the
- default handler is just an IRET. But see sections »» 6.33 and »» 6.35.
-
- See the interrupt handlers in the sample programs for more details.
-
- If you use the CALL method to chain to the old interrupt, in an int 8 handler,
- beware that there may be a TSR using the Long Tick Interrupt technique described
- in section »» 6.9, which will send an EOI but not return, thus causing your
- interrupt handler to be re-entered while it was part-way through execution.
- You probably should design the handler to support this possibility.
-
- If you use the JMP chaining method, this consideration does not apply.
-
- ## 6.32 WRITING INTERRUPT HANDLERS IN ASSEMBLY LANGUAGE
-
- Here are some guidelines and warnings that you should heed if you are coding
- an interrupt handler in assembly language.
-
- On entry to the interrupt handler, the only registers that will be known are
- CS and IP. DS is undefined. You must preserve any other registers that you
- modify, except the flags (which will be restored by the IRET).
-
- Also see sections »» 6.23 to »» 6.26 for restrictions on what may be called
- from, and done inside, your interrupt handler.
-
- ## 6.32.1 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: ACCESSING VARIABLES
-
- If your interrupt handler must have access to memory variables, such as flags,
- structures or buffers used to communicate with other parts of your program,
- there are three main ways to do this:
-
- ■ Common code and data segment (COM files; tiny model), access with CS
- ■ Put variables in code segment and access them using CS
- ■ Put variables in data segment and set DS so you can access them.
-
- The first approach is used in single-segment COM-files (also known as tiny
- model) in which the code and data segment-paragraphs are the same. In these
- programs, CS will already address the segment (because the interrupt handler
- is in the same segment as the data), so you can access variables using CS.
- This is done via the ASSUME directive, which tells the assembler what segment
- each of the segment registers is supposed to contain. The directive:
-
- ASSUME cs:_TEXT,ds:nothing,es:nothing,ss:nothing
-
- tells the assembler that only the CS register is known at the moment, and that
- CS addresses the _TEXT segment. You would change the name to whatever segment
- you use for your single segment. This directive should appear before the
- interrupt handler. The ASSUMEd registers remain in effect until modified by
- another ASSUME directive.
-
- The above ASSUME directive tells the assembler that only the _TEXT segment is
- addressable, and that every access to a variable in that segment will require
- a CS segment override prefix. You need not explicitly code the CS override on
- every instruction - the assembler takes care of this automatically (unless
- you're using A86 :-) But be very careful with string instructions, because
- they don't make references to data objects, and an explicit segment override
- may be required. For example, if only CS is ASSUMEd:
-
- mov ax,SomeVariable ; This will generate a CS
- ; override and will work,
-
- mov si,OFFSET SomeTable ; Point to start of table
- lodsw ; Uh-oh! No override will
- ; be generated on this!
-
- lodsw cs: ; This will work
-
- The second method is used with multiple segment programs. The variables are
- placed in the code segment, typically near to the interrupt handler, and are
- accessed in a similar way. An ASSUME directive should be used to tell the
- assembler that CS is the only known segment register, and that it addresses
- the code segment (_TEXT or whatever). The trouble with this method is that
- the main program has to access those variables in the code segment, which is
- messy.
-
- This second method is most often used in large assembly language programs.
-
- The third method is the method used in C programs and most high level programs,
- where placing variables in the code segment is frowned upon and/or impossible.
- The variables are placed in the data segment, as normal. This requires that
- somehow, a segment register (typically DS) must be loaded with the appropriate
- segment at some point in the interrupt routine, before those variables are
- addressable. This also requires that the segment register (e.g. DS) is pushed
- at the start of the interrupt handler and popped again before it terminates.
-
- Again, an ASSUME directive should be used to tell the assembler what segment
- registers point to what. Here is a sample code fragment:
-
- -------------------------------- snip snip snip --------------------------------
- DATA SEGMENT
- SomeVariable DW 0 ; Some variable, used by int. handler
- AnotherVar DW 0 ; Another variable, ditto
- DATA ENDS
-
- CODE SEGMENT
-
- ASSUME cs:CODE,ds:nothing,es:nothing,ss:nothing
-
- MyIntHandler PROC far
- pushf ; Preserve flags
- push ax ; Preserve register
- push ds ; Preserve DS
- mov ax,SEG DATA ; Get data segment to AX
- mov ds,ax ; Move it to DS
- ASSUME ds:DATA ; (CS, ES and SS are unchanged)
- mov ax,SomeVariable ; Get some variable
- add ax,AnotherVar ; etc, you get the idea...
- ; -- More code here
- pop ds ; Restore DS
- ASSUME ds:nothing ; Cannot address anything useful with DS
- pop ax ; Restore AX
- popf ; Restore flags
- DB 0EAh ; JMP xxxx:yyyy
- OldIntOfs DW 0 ; Vector to original handler - Offset
- OldIntSeg DW 0 ; Segment
- MyIntHandler ENDP
- -------------------------------- snip snip snip --------------------------------
-
- I suggest using tiny model for assembly language programs (avoids segment
- register setting in the interrupt handler), or for assembly language programs
- in other models, place the variables in the code segment, and for C programs,
- place them in the data segment. This is a matter of personal preference,
- though.
-
- ## 6.32.2 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: STARTING CONDITION
-
- The interrupt flag in the flags register will be clear (i.e. interrupts
- locked out) unless a badly behaved interrupt handler has chained to your
- interrupt handler but left interrupts turned on. If you are doing critical
- hardware access in your handler, you may want to issue a CLI just in case.
- This should not apply to int 8, as it is the highest priority interrupt and
- should never be interrupted (except by an NMI!)
-
- You may have noticed that I pushed and popped the flags in the sample code
- in section »» 6.32.1. This is probably not necessary in such a case as the
- original flags are popped by the IRET at the end of the old interrupt handler
- that is being chained to via the JMP instruction (see section »» 6.31), but I
- think it's wise to make sure that the chained interrupt handler starts with
- the same flags that it would have had if our interrupt handler was not present.
-
- The direction flag will probably be clear, but DON'T COUNT ON IT! If you do
- any string manipulation in your interrupt handler, be sure to include a CLD
- instruction to ensure that the direction flag is known. Forgetting this
- precaution is an open-armed invitation to subtle intermittent bugs.
-
- ## 6.32.3 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: PRESERVE THE REGISTERS
-
- Of course, your interrupt handler must not modify any registers - use PUSH and
- POP to preserve the old values in registers if you need to use the registers
- for something else.
-
- Watch out for instructions that modify unexpected registers - for example
- the 16-16-32 MUL instruction modifies DX; even if you don't _use_ the high
- word of the result, DX will still be modified.
-
- ## 6.33 USING INTERRUPT EIGHT IN A TSR
-
- You must intercept int 8 when speeding up the timer tick (see section »» 8).
- Int 8 can also be used by TSRs which want a regular interrupt source. TSRs
- should not use int 1Ch, though some do - see section »» 6.35.
-
- On installation, your TSR should obtain the contents of the int 8 vector using
- getvect() or DOS function 35 hex, and store it, then replace the interrupt
- handler with its own handler.
-
- Every time the TSR's int 8 handler is called, it must chain to the old interrupt
- handler, usually by jumping to it, as described in section »» 6.31. Your TSR
- then has a regular 54.9254 millisecond interrupt source. If a foreground
- program reprograms the timer tick for a faster rate (see section »» 8), calls
- to your int 8 handler may be unevenly spaced. In the worst case, it is
- possible for int 8 invocations to be spaced as closely as 27.4627 ms, half the
- normal spacing, and as far apart as 82.3881 ms, 1.5 times the normal spacing.
- Over a period of time the interval between int 8 calls should average out to
- 54.9254 ms, though. See sections »» 6.23 to »» 6.26 for details of restrictions
- and techniques for interrupt handlers.
-
- If your TSR can be uninstalled from the command line (a useful feature), the
- original int 8 vector contents must be restored, but before restoring vectors
- when uninstalling, ensure that the int 8 vector, and the vectors for any other
- interrupts your TSR intercepts, are currently pointing to the handler in the
- installed copy of the TSR. If they do not, one or more TSRs have been loaded
- after your TSR, and it is not safe to uninstall your TSR because restoring the
- interrupt vectors will unhook the other TSRs and sabotage their operation. In
- this case, you must advise the user that the TSR cannot be uninstalled as other
- TSRs have been installed above it.
-
- I believe there is a package called Tesseract, or maybe AMIS, written by Ralf
- Brown of the Interrupt List fame, which provides a general TSR template and
- also permits compatible TSRs (i.e. ones written to be compliant with the
- system) to be unloaded in any order. This sounds like a good idea, though I
- have not used it. I found ftp://oak.oakland.edu/SimTel/msdos/info/altmpx35.zip
- which is dated 13 Sept 1992, but I don't know if this is the latest version.
- If someone knows the latest version and its home site, please advise me so I
- can include a reference here. (*)
-
- ## 6.34 USING INT 8 WITHOUT CHAINING
-
- In some cases, for minimal interrupt overhead when int 8 is being operated at
- a high rate, it may be necessary to use int 8 without chaining. Doing this
- will cause the DOS time to freeze (unless an RTC-based CLOCK$ driver such as
- ATRTC, see section »» 3.3 is installed), will prevent floppy disk drives from
- turning off after two seconds of inactivity, will probably prevent timeout-
- based 'green' functions (slow-mode, hard drive spindown on laptops, etc) from
- kicking in, and will probably break the mouse driver and any network software,
- as well as most screen savers and some pop-up TSRs, so this is not something
- that should be done by a well-behaved program that is intended for general use.
-
- You can see the effect of disabling the timer interrupt by using the sample
- program in section »» 7.12 to set CTC channel zero to an inappropriate mode,
- such as mode zero, thus stopping the timer tick.
-
- ## 6.35 USING INT 1C HEX INSTEAD OF INT 8
-
- Int 1Ch is intended for use by user programs for timing. It is invoked 18.2065
- times per second by the BIOS int 8 handler. On entry to the handler, interrupts
- will be disabled. Do not issue an EOI command to the interrupt controller - the
- BIOS int 8 handler takes care of this after the int 1Ch handler returns.
-
- TSRs should not use int 1Ch - see below for a discussion of this.
-
- In theory, you should not need to chain to the original int 1Ch handler, as the
- default handler is a dummy handler, simply an IRET. However, some existing
- TSRs hook int 1Ch. For compatibility with those TSRs you should make your
- non-TSR programs chain to the old int 1Ch handler if they use int 1Ch.
-
- See sections »» 6.23 to »» 6.26 for details of what can, and cannot, be safely
- done inside an int 1Ch handler.
-
- To use int 1Ch, during initialisation the program should store the address of
- the original int 1Ch handler and replace the old handler with a new one, and
- on termination, the program should restore the old handler address. Chain to
- the old handler in the normal way on every int 1Ch call. It does not matter
- whether you chain before you perform your own processing, or after.
-
- A program which intercepts int 8 or int 1Ch should trap critical errors and the
- DOS Ctrl-C vector, and optionally the Divide Overflow vector, so that if the
- program is terminated due to a critical error or a user Ctrl-Break or Ctrl-C,
- the interrupt vector can be restored to its original address as part of the
- clean-up. See section »» 5 and subsections for details on trapping the Ctrl-C
- and critical error vectors.
-
- In my view, it is inappropriate for a TSR to hook int 1Ch. Some people have
- disagreed with this opinion, so for their benefit I will present the evidence
- that I have found, and explain the logic by which I arrived at my conclusion.
-
- 1 The MS-DOS Encyclopedia has two articles that relate to interrupts and TSRs.
- This book is published by Microsoft Press and edited by Ray Duncan. The two
- relevant articles are Article 11 on TSRs by Richard Wilton, and Article 13
- on Hardware Interrupt Handlers by Jim Kyle and Chip Rabinowitz. Article 11
- has a TSR example which uses int 8. The article makes no mention of int 1Ch
- at all. Article 13's example code also uses int 8 and the article only
- mentions int 1Ch in a table of low-numbered interrupts as "Timer tick (user
- defined)".
-
- 2 The PC and XT technical references have the following to say about int 1Ch:
- "This vector points to the code to be executed on every system-clock
- tick. This vector is invoked while responding to the timer interrupt,
- and control should be returned through an IRET instruction. The power-
- on routines initialise this vector to point to an IRET instruction, so
- that nothing will occur unless the application modifies the pointer.
- It is the responsibility of the application to save and restore all
- registers that will be modified."
-
- 3 From the book "DOS Programmer's Reference", 3rd edition, published by Que
- Corporation, written by Dettmann, Kyle and Johnson (see section »» 12), in
- the section on int 8:
- "Int 08h, which is called 18.2 times per second to advance the time-of-
- day counter, is tied directly to channel 0 of the system timer chip.
- People who write TSRs with utilities such as SideKick, for example,
- find Int 08h particularly useful for time-related triggering (as with
- a clock or alarm). This interrupt calls Int 1Ch (Timer Tick).
- Most TSRs should connect to Int 1Ch rather than to Int 08h.
- In the section on int 1Ch:
- "Vector 1Ch, the timer tick interrupt called by int 8 (system
- metronome), is initialised to point to an IRET instruction. A TSR
- that needs to be triggered at each clock tick can reset the vector
- for this interrupt to point to a custom interrupt handler.
- "Because this function is called from inside the int 08h code, before
- handling of that top-priority action is completed, it shares top
- priority and will prevent the system from responding to any other
- hardware interrupt requests, including those from serial devices
- or disk units, while it executes. Therefore it is necessary to keep
- to an absolute minimum the time spent in any handler for this function,
- or you will risk the loss of data when time-sensitive applications
- are running.
- "The best practice for a TSR is merely to set a flag from this function,
- then inspect the flag from another handler hooked into the int 28h
- (DosOK) chain, which gives ample time to take care of any needed
- processing without blocking hardware interrupts."
- In a section on TSR programming:
- "If DOS is not waiting for input, you can use the timer interrupt. The
- timer interrupt (1Ch) ticks 18.2 times per second. You can attach to
- this interrupt in the following service routine that checks the hot-key
- flag as well:
- "Timer Interrupt activates
- "Call next timer interrupt service
- ...
-
- The TSRs in the MS-DOS Encyclopedia use int 8 but do not say that int 8 should
- be used, and do not give reasons. The DOS Programmer's Reference states clearly
- that int 1Ch should be used by TSRs but do not give reasons, and its section on
- int 1Ch is worded so as to imply that int 1Ch should not be chained if used in
- a TSR, though it is obvious (and clearly shown in the TSR programming section of
- the same book) that it should be chained. Since the MS-DOS Encyclopedia is
- sanctioned by Microsoft and edited by Ray Duncan, I feel it has more weight
- (particularly the hardback edition :-) than the Que book, even though Jim Kyle,
- one of the authors of the Que book, co-designed the AMIS TSR interface! The
- technical reference also makes the point that the default handler for int 1Ch
- is an IRET, and clearly states that "nothing will occur unless the application
- modifies the pointer", though this text was written before TSRs were commonplace
- and is probably not written with TSR considerations taken into account.
-
- In my view, TSRs should not use int 1Ch, they should use int 8. Applications
- may use either (though they must use int 8 if they are speeding up the timer
- tick; this is a separate issue). If an application hooks int 8, it must chain
- to the original handler. If an application hooks int 1Ch, it should also chain
- to the original handler, to support existing TSRs which use int 1Ch.
-
- My logic in coming to this conclusion is:
-
- Int 1Ch is (or was originally) defined for _user_ program use,
- The default handler is an IRET, and was provided simply to keep
- the machine from crashing when int 1Ch is issued by the
- BIOS int 8 handler and no user program is using int 1Ch,
- Therefore an application grabbing int 1Ch does not need to chain,
- Therefore a TSR writer should not assume that int 1Ch will be chained,
- Therefore a TSR writer should use int 8, not int 1Ch.
- Some TSRs do use int 1Ch,
- Therefore an app using int 1Ch should chain, to support these TSRs.
-
- I believe that a TSR should operate as transparently as possible, i.e. the
- environment presented to a user program should be the same with or without the
- TSR. The default handler for int 1Ch is an IRET, so an application does not
- need to chain when it hooks int 1Ch. If a TSR hooks int 1Ch, the default int
- 1Ch handler (from an application program's point of view) is no longer an IRET,
- and the new 'default' handler must be chained. This has changed the environment
- from one where the default handler was just provided so that the machine didn't
- crash and there was no reason to chain, to one where chaining is required.
- Therefore I regard this as bad programming practice for a TSR writer.
-
- As to the question of whether an application program should chain int 1Ch,
- there are clearly some TSRs in existence that do use int 1Ch, so application
- programs should now chain int 1Ch. In my opinion this is unfortunate, but due
- to the number of programmers who write DOS software, and the lack of thorough
- documentation on TSR writing from IBM and Micro$oft, such misunderstandings and
- design misfeatures are a sad fact of life. There are other cases where programs
- must go to lengths to do things they shouldn't have to do, in order to work
- around problems due to bad design in other programs - for example, the old DOS
- VDISK program, which grabbed extended memory uncooperatively because it was
- written before the XMS standard evolved, is a good example - memory managers
- must check for its existence explicitly and refuse to install if VDISK is found.
-
- If you think that this lack of coordination is surprising, consider that an
- organised software company developing an operating system would provide its
- programming staff with a thorough design document, and allow only experienced
- system-level programmers to work on the interrupt routines, whereas Micro$oft
- has provided precious little documentation on writing reliable low-level code,
- and (because of DOS's lack of support for anything more esoteric than file and
- memory management), forced large numbers of programmers with varying amounts of
- low-level programming experience to 'go to the hardware' when they want a fast
- tick interrupt or a serial port that can operate faster than 300 baud :-) With
- this sorry state of affairs, it's a miracle that so many TSRs can live together
- at all!
-
- ## 6.36 SAMPLE PROGRAM: USING INT 1CH WITH CRITICAL ERROR AND CTRL-C HANDLING
-
- The following program demonstrates using int 1Ch and handling critical errors
- and Ctrl-C using the critical error handling module from section »» 5.8.
-
- The program traps Ctrl-C (it has its own Ctrl-C handler) and critical errors
- (via the crit_err_intercept() function in CRIT_ERR.ASM), and takes over the
- int 1Ch interrupt. It does not chain to the original int 1Ch handler, as this
- is supposed to be a dummy IRET instruction. If a badly written TSR is using
- this interrupt, then it will just have to miss out while my program is running
- (see section »» 6.35 for more details).
-
- Every timer tick, it toggles the speaker state, causing a ticking noise.
- The speaker toggle is done inside new_int_1Ch().
-
- First, the user has the opportunity to press Ctrl-C while DOS function 1 is in
- progress. This triggers the Ctrl-C handler, which terminates the program with
- the message "Program terminated by Ctrl-Break or Ctrl-C". The abort_cleanup()
- function is called with dos_is_safe set to TRUE.
-
- If the user presses Enter instead of Ctrl-C, the program continues, and tries
- to open the file "A:NOSUCH.FIL". Leave the disk drive empty for this test.
- This invokes the critical error handler and issues the Abort, Retry, Ignore
- (or Fail) prompt. If the user selects Abort, the critical error intercepter
- (in CRIT_ERR.ASM) calls abort_cleanup() with dos_is_safe FALSE, then returns
- the Abort error code to DOS, which terminates the program. If the user selects
- Fail, the program continues, and calls abort_cleanup() with dos_is_safe TRUE,
- then terminates normally via exit(0).
-
- abort_cleanup() resets the control signals for the speaker and cleans up the
- int 1Ch vector. If DOS is not safe, it restores the vector directly by patching
- the interrupt vector table directly. It only attempts to restore the int 1Ch
- vector if the old_int_1Ch variable is not equal to 0xFFFFFFFF, i.e. the vector
- has actually been intercepted!
-
- In any case, the ticking sound should stop when the program terminates for any
- reason, indicating that the interrupt vectors were correctly restored and the
- machine is in a stable state.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #4
- Demonstrates using int 1Ch and handling Ctrl-C and critical errors
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save and assemble the critical error module CRIT_ERR (above)
- Save this sample code to SAMPLE4.C
- Compile this module with:
- bcc -c -I<inc_path> -ms sample4.c
- Link the modules with:
- tlink /c /x <c0_path>\c0s.obj sample4.obj crit_err.obj,
- sample4, nul, <lib_path>\cs
- Where inc_path is the path to your C header files, c0_path is the path to your
- startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #include <dos.h> /* Needed for enable(), disable(), MK_FP() */
- #include <fcntl.h> /* Needed for O_RDONLY */
- #include <io.h> /* Needed for _open() and _write() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDERR 2 /* DOS handle for standard error */
-
- void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
- unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
-
- typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
-
- intfuncp old_int_1Ch = (intfuncp)0xFFFFFFFFL;
-
- void abort_cleanup(int dos_is_safe) {
- if (dos_is_safe) {
- if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
- setvect(0x1C, old_int_1Ch);
- old_int_1Ch = (void far *)0xFFFFFFFFL;
- }
- /* Insert other cleanups here - DOS can be safely called */
- }
- else {
- disable(); /* Probably superfluous */
- if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
- *((intfuncp far *)MK_FP(0, 0x1C << 2)) = old_int_1Ch;
- old_int_1Ch = (void far *)0xFFFFFFFFL;
- }
- /* Insert other cleanups here - DOS can NOT safely be called */
- }
- outportb(0x61, inportb(0x61) & 0xFC); /* Clean up speaker control */
- return;
- }
-
- void interrupt ctrl_c_handler(void) {
- static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
- if (is_at_crit_prompt())
- abort_cleanup(FALSE);
- else {
- abort_cleanup(TRUE);
- _write(STDERR, &message, sizeof(message));
- }
- exit(255);
- }
-
- void interrupt new_int_1Ch(void) {
- outportb(0x61, (inportb(0x61) & 0xFE) ^ 0x02);
- return; /* From interrupt */
- }
-
- void intercept_int_1Ch(void) {
- old_int_1Ch = getvect(0x1C);
- setvect(0x1C, new_int_1Ch);
- return;
- }
-
- unsigned int dos_func_1(void) {
- _AX = 0x100;
- geninterrupt(0x21); /* DOS keyboard input with echo and break */
- return _AL;
- }
-
- void main(void) {
- int n;
-
- printf("Sample program #4 - Demonstrates using int 1Ch and handling Ctrl-C and critical errors\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
-
- crit_err_intercept(); /* Trap critical errors */
- setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
- intercept_int_1Ch(); /* Intercept int 1Ch */
- printf("Type characters, press Ctrl-C to test the Ctrl-C handler\n");
- printf("Press Enter to continue\n");
- do {
- n = dos_func_1();
- } while (n != '\r'); /* Wait for C/R */
- printf("Now testing critical error handler, opening 'A:NOSUCH.FIL'\n");
- printf("Please remove any disk (if any) from drive A\n");
- printf("Select the Abort option to test the critical error handler\n");
- n = _open("a:nosuch.fil", O_RDONLY);
- if (n != 0 && n != -1)
- _close(n);
- abort_cleanup(TRUE);
- printf("Normal program termination\n");
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 6.37 DEBUGGING INTERRUPT HANDLERS
-
- Saul Cozens (s.cozens@sheffield.ac.uk) wrote:
-
- > I have noticed that many people attempt to debug interrupt handlers by
- > adding a printf statement so they know that the ISR has been called.
-
- Yes, of course printf() is right out. printf() calls DOS (usually), therefore
- it cannot safely be used from within an interrupt handler such as an int 8
- handler. Saul suggests that the BIOS 'write string' function is a safer bet.
- The BIOS video functions are listed as non-reentrant, but often you can get
- away with calling them from an interrupt handler. A common technique when
- trying to figure out just _what_ an interrupt handler is doing, is to issue
- a bell at appropriate points, using int 0x10, function 0x0E with 0x07 in AL.
- For a cleaner approach, on every interrupt just clear the Timer 2 Gate bit on
- Port B and toggle (flip) the Speaker Enable bit. This will produce a click on
- each interrupt. Alternatively, increment a character in screen memory. The
- character at offset 0F00h into the regen buffer is the bottom left corner
- character in 80x25 text modes.
-
- > I found a bug with Borland C 3.1. When a function is declared as an
- > interrupt function, the compiler quite rightly perserves all the registers
- > automatically (I used Turbo Debugger to look at the compiled code).
- > Unfortunately it does not save the high words of the 32-bit registers, even
- > when the options are set to 'use 32-bit registers'.
-
- I have also noticed this problem - in Borland Pascal 7. A library arithmetic
- function (long div) uses 32-bit registers, if the appropriate compiler option
- is set. If this function is called from within an interrupt routine, the
- hiword of EAX is destroyed. The 'interrupt' keyword on the function definition
- causes the 16-bit registers to be preserved on the stack, but not the 32-bit
- registers. This bug is particularly nasty because this function is called
- invisibly to the programmer, as part of an innocent-looking calculation.
- There may also be implications when running with a DOS extender.
-
- John Stockton (jrs@dclf.npl.co.uk) sent me the following information which
- contains a fix for this problem and also mentions another related problem:
-
- > Duncan Murdoch (dmurdoch@mast.queensu.ca) has provided inline TP/BP code
- > to save and restore EAX..EDX in an ISR:
- >
- > procedure PushEAXtoEDX ; {from DM}
- > Inline(
- > $66/ {db $66} $50/ {push ax}
- > $66/ {db $66} $53/ {push bx}
- > $66/ {db $66} $51/ {push cx}
- > $66/ {db $66} $52 {push dx} ) ;
- >
- > procedure PopEDXtoEAX ; {from DM}
- > Inline(
- > $66/ {db $66} $5A/ {pop dx}
- > $66/ {db $66} $59/ {pop cx}
- > $66/ {db $66} $5B/ {pop bx}
- > $66/ {db $66} $58 {pop ax} ) ;
- >
- > and he has pointed out that something like:
- >
- > var X, Y : longint ;
- > {...}
- > X := 10 ; Y := 10 ;
- > { repeatedly : } if X * Y <> 100 then BEEP ;
- >
- > in the main program can detect this problem and its cure.
- >
- > Matters are worse if the main program and the ISR both use the hardware
- > FPU programmed in Pascal. One can save and restore the FPU state, and
- > that does help but does not cure the problem. It seems that TP/BP FPU
- > code uses non-reentrant 80x86 routines around 80x87 instructions.
- >
- > Inspiration dawned during an E-mail exchange with Norbert Juffa
- > (norbert@itt.com, whose files should be read by anyone interested
- > in Pascal and floating point).
- >
- > I (with a '486) now compile the ISR in the {$N-,E-} state which forces
- > 6-byte software real arithmetic, and the main code in the {$N+,E-} state
- > using extended variables and hardware arithmetic. With a little care to
- > disable interrupts while transferring values between types real and
- > extended, all seems well.
-
- Thank you John for that information.
-
- > Another thing that used to catch me out was that single stepping (using
- > Turbo Debugger) through bits of code that re-program the PIT causes a
- > system crash. This is presumably because the writes to certain registers
- > must be consecutive and the Turbo Debugger writes to the PIT itself every
- > time it does a 'step'.
-
- When programming the CTC, the entire access sequence must be completed without
- interference, so interrupts must be locked out, and the sequence of accesses
- must be executed from start to finish without being interrupted by anything,
- including a debug single step interrupt or breakpoint.
-
- This section may be improved later (*)
-
- ## 7 HARDWARE INFORMATION AND PROGRAMMING
-
- ## 7.1 THE 14.31818 MHZ CLOCK
-
- A crystal oscillator or oscillator module generates a 14.31818 MHz clock which
- is divided by 12 to give the 1.1931816666666... MHz clock frequency (period is
- 12/14318180, or 0.83809534452 us), which is fed to all three channels of the
- counter/timer chip. This is the basic timing resolution of the counter/timer.
-
- ## 7.2 CLOCK FREQUENCY ACCURACY
-
- The 14.31818 MHz clock's absolute accuracy depends mainly on the quality of the
- 14.31818 MHz crystal or crystal oscillator module, and is typically in the
- region of +/- 5 ppm (0.0005%; 0.4 seconds per day) to +/- 20 ppm (0.002%; 1.73
- seconds per day). Errors consist of initial frequency error, and variations
- due to temperature and long-term drift. Because of these inaccuracies, there
- is little point in specifying times or frequencies to more than five or six
- digits as I have done above.
-
- If required, frequency accuracy can be improved by installing a high quality,
- close-tolerance crystal, or a high quality crystal oscillator module, which
- will reduce all of the above error sources. If accuracy is still inadequate,
- with a crystal it may be possible to add a small variable capacitor to the
- oscillator circuit, to 'pull' the crystal onto the correct frequency. If
- anyone has specific advice on this, please let me know. (*)
-
- Alternatively, your software could incorporate an adjustment so that once the
- amount of error has been measured, manually by the user over a long period of
- time, it could be corrected by the software. Of course this must be configured
- individually for every machine the software will run on, and temperature and
- long term drift will still have an effect.
-
- Historical note: If you were wondering "Wouldn't 1 MHz have been easier?", yes
- it would, but that would have required an extra crystal. IBM were... er,
- 'clever' - they used a master clock of 14.31818 MHz, and used logic chips to
- derive the 4.77 MHz CPU clock, the timer clock, and the NTSC colour subcarrier
- frequency for the CGA card, so they could save a few dollars. Although the
- 14.31818 MHz signal is not required by modern CPUs and video cards (in fact,
- it is now only used for the CTC clock!), the strange frequency still hangs
- around like a stale fart - we are stuck with it forever. :-(
-
- ## 7.3 THE COUNTER/TIMER CHIP (CTC)
-
- The counter/timer chip (CTC) in the IBM PC family is an Intel 8253 in the PC and
- XT, or an Intel 8254 in the AT and later machines (except the PS/2 {JAM}) or a
- functional equivalent, and is part of the processor support chipset on the
- motherboard. On modern motherboards, it is part of one ASIC in a chipset.
-
- The CTC has three fully independent channels, numbered zero, one, and two.
- Each has a clock input, a gate input, and an output, and in the PC, family,
- these are wired as follows:
-
- Chan Clock input Gate input Output Channel is used for
- ---- ----------- ---------- ------ -------------------
-
- 0 1.193182 MHz Tied high To IRQ0 Timer tick
- 1 1.193182 MHz Tied high DRAM refresh DRAM refresh
- 2 1.193182 MHz Timer 2 Gate Speaker gating Audio generation
-
- Software access to the CTC is via four adjacent addresses in the directly
- addressable I/O page. Programming information starts at section »» 7.9.
-
- In most respects the 8253 and 8254 are identical. The following description
- applies to both types of CTC unless specifically stated.
-
- ## 7.4 CTC CHANNELS
-
- Each channel operates independently, and can be programmed for one of six modes
- of operation. Normally, modes 2 or 3 are used. In these modes, the CTC channel
- takes the CTC clock (1.193182 MHz) and 'divides' this frequency down to produce
- a lower frequency at the output pin. Other modes operate differently.
-
- The frequency division is controlled by the 'divisor' value, a 16-bit unsigned
- number between 1 and 65536 (65536 is represented as zero), which is individually
- programmable for each channel in the CTC. Setting a very small divisor value
- gives a very high output frequency. A divisor of 65536 gives the lowest output
- frequency, 18.206507364909 Hz (cycle period is 54.92541649846559 ms).
-
- ## 7.4.1 CTC CHANNEL ZERO
-
- CTC channel zero normally operates in mode two or three with a divisor of
- 65536, giving an output frequency of 18.2065 Hz (period is 54.9254 ms). Its
- gate input is tied high. Its output drives the IRQ0 input of the primary PIC
- (8259 interrupt controller chip). On every rising edge of the channel zero
- output pin (i.e. transition from low to high), IRQ0 is triggered, invoking
- interrupt 8, the timer tick interrupt (see section »» 6.1).
-
- ## 7.4.2 CTC CHANNEL ZERO DEFAULT OPERATING MODE
-
- Traditionally, CTC channel zero has been set to operate in mode three by the
- BIOS POST, but recent 486 BIOSes that I have seen appear to be using mode two
- by default. The only significant differences are the width of the pulse from
- the CTC pin that triggers the timer tick interrupt, which is narrow in mode
- two but is still plenty wide enough for the Intel 8259 PIC chip, and the value
- read from the CTC channel zero counter (which decrements twice as quickly in
- mode 3).
-
- From a hardware point of view, either mode should work on all motherboards, but
- if some code in the BIOS assumes that CTC channel 0 is in the mode that the BIOS
- originally programmed, it may not work correctly if CTC channel 0 has been
- reprogrammed for the other. Of course, reprogramming the CTC divisor for a
- higher sample rate will also cause this problem. The only example of this that
- I know of, is the joystick read function (int 15h called with AH = 84h and
- DX = 1) (see section »» 10.4.2). Please tell me if you find any other problems
- related to changing the mode. (*)
-
- ## 7.4.3 CTC CHANNEL ONE
-
- CTC channel one triggers DRAM refresh cycles. DRAM (Dynamic Random Access
- Memory) is the main system memory in your computer (typical machines have four
- to eight megabytes of RAM, or 32 to 64 megabytes if you want to run Win 95 :-)
- DRAM stores data as electrical charges on tiny capacitors inside the chip, and
- this type of memory must be refreshed regularly to prevent the capacitors from
- discharging. On the PC and XT, refresh cycles are implemented via the DMA
- controller. On the AT and later machines, refresh cycles are performed by
- dedicated hardware. It appears that the AT does use CTC channel one to
- initiate DRAM refreshes, but I have heard that you cannot change the refresh
- rate on ATs and later machines. Can anyone shed any light on this? (*)
-
- The normal divisor for CTC channel one is 18, which gives a DRAM refresh cycle
- every 15.0857162013608 microseconds. Every refresh cycle forces the processor
- to wait briefly, and a popular trick used to be to slow down the DRAM refresh
- rate on PCs and XTs by increasing the divisor, to reduce the refresh overhead,
- giving a few percent performance improvement, so your flash 8MHz Turbo XT would
- actually seem to run at 8.05 MHz! Seems pretty pathetic now, doesn't it :-)
-
- CTC channel one is not even accessible on the PS/2's ASIC {JAM}.
-
- CTC channel one has no interrupt connection, but can be used for timing via the
- Refresh Detect signal on bit 4 of Port B. See section »» 7.37.
-
- ## 7.4.4 CTC CHANNEL TWO
-
- CTC channel two generates audio for the speaker. It is the most versatile CTC
- channel, because its gate input can be controlled by software, and its output
- can be read by software via Port B (see section »» 5.5).
-
- CTC channel two can be used for timing, but it cannot generate an interrupt.
- See section »» 7.29 and section »» 7.31 for examples of programming CTC channel
- two. See the section »» 5.5 for details of the speaker interface.
-
- ## 7.5 SPEAKER INTERFACE
-
- The speaker interface on the PC and XT is implemented via the 8255 PPI chip,
- which occupies I/O addresses 60h to 62h inclusive, and also provides the
- interface to the keyboard. Port B (read/write, at I/O address 61h) and Port
- C (read-only, at I/O address 62h) are used by the speaker interface.
-
- On the AT and later machines, which do not have a PPI chip, these functions are
- implemented in an ASIC in the chipset, or with discrete logic, as a partly
- read-only, partly read/write register at I/O address 61h, known as Port B.
-
- In most respects, the PC/XT and AT interfaces are similar. CTC channel two
- gate input can be controlled by software via a read/write bit in an I/O
- register; this signal is known as Timer 2 Gate. The CTC channel two output
- pin can be read back directly, via a read-only bit in an I/O register, and is
- AND-gated with a signal called Speaker Data (software controlled, via a
- read/write I/O register bit), the speaker being driven from the output of the
- AND gate, sometimes via a simple resistor-capacitor lowpass filter to remove
- high frequency components. On the PC and XT only, the speaker control signal
- (after the AND gate, and inverted) can also be read back by software, though
- this seems to be an undocumented feature and may not work on all machines.
-
- Figure 1 (FIGURES archive) shows the speaker interface signals and circuitry.
-
- PC and XT : I/O address 61h, "PPI Port B", read/write
- 7 6 5 4 3 2 1 0
- * * * * * * . . Not relevant to speaker - do not modify!
- . . . . . . * . Speaker Data
- . . . . . . . * Timer 2 Gate
-
- PC and XT : I/O address 62h, "PPI Port C", read only
- 7 6 5 4 3 2 1 0
- * * . . * * * * Not relevant to speaker, read-only
- . . * . . . . . Timer 2 output read-back
- . . . * . . . . Speaker signal (after AND gate, inverted), undocumented
-
- AT and later : I/O address 61h, "Port B", partly read/write, partly read-only
- 7 6 5 4 3 2 1 0
- * * . . . . . . Not relevant to speaker, read-only
- . . * . . . . . Timer 2 output read-back, read-only
- . . . * . . . . Refresh Detect (read-only), see section »» 7.37
- . . . . * * . . Not relevant to speaker - do not modify! (read/write)
- . . . . . . * . Speaker Data (read/write)
- . . . . . . . * Timer 2 Gate (read/write)
-
- I have a nasty suspicion that the PS/2 may not implement Port B properly. Can
- anyone confirm or deny this? (*)
-
- Audio generation can be done via CTC channel two, by setting Timer 2 Gate high
- and Speaker Data also high. This enables channel two, and enables its output to
- control the speaker directly. Alternatively, if Timer 2 Gate is set low, CTC
- channel two output goes high (assuming an appropriate mode is programmed for
- channel two), and Speaker Data can be manipulated to drive the speaker directly.
-
- The former technique is used in the sample program in section »» 7.30.
-
- Here is a code fragment that determines whether the speaker hardware is the
- PC/XT type or the AT type. It uses bit 7 of the I/O port at 61h. On an XT,
- PPI Port B is fully read/write, and bit 7 is the keycode acknowledge signal to
- the keyboard interface on the motherboard. On an AT, bits 4-7 of Port B are
- read-only, and bit 7 is the motherboard RAM parity error signal. By toggling
- bit 7 six times and testing whether the port reads the expected value, this
- code fragment determines what type of Port B hardware and keyboard interface
- is present. This code destroys AX and CX.
-
- -------------------------------- snip snip snip --------------------------------
- pushf ; Keep interrupt flag
- mov cx,400h ; Six attempts (top bits of CH)
- cli ; Lock out interrupts during this stuff
- in al,61h ; Get Port B contents
- jmp SHORT $+2 ; Short delay
- mov ah,al ; Original value to AH
- Flip61Loop: xor ah,10000000b ; Flip top bit
- mov al,ah ; Get value to AL
- out 61h,al ; Write value to port
- jmp SHORT $+2 ; Short delay
- jmp SHORT $+2 ; Short delay
- in al,61h ; Read it back
- xor al,ah ; Set bit 7 if value didn't stay
- shl al,1 ; Shift bit into carry
- rcl cx,1 ; Shift bit into bottom of CX
- jnc Flip61Loop ; Loop if more flips (six in total).
- popf ; Restore interrupt flag
- test cl,cl ; Was port read/write? Zero if so.
- -------------------------------- snip snip snip --------------------------------
-
- This code fragment will leave the zero flag true if the machine is a PC or XT
- (i.e. Port B bit 7 is read/write), or zero flag false if the machine is an AT
- or later machine (i.e. Port B bit 7 is read-only). You could follow it with
- the instruction:
-
- jnz Not_PCXT ; If not, it's an AT
-
- ## 7.6 CTC INTERNAL REGISTERS
-
- Each CTC channel operates independently. Each channel contains:
-
- ■ A 6-bit Mode register
- ■ A 16-bit Reload register (the 'divisor register' in modes 2 and 3)
- ■ A 16-bit Counting register (the 'Counting Element' in Intel docs)
- ■ A 16-bit Latch register
- ■ An 8-bit Status Latch register
- ■ A lobyte/hibyte flag
- ■ A 'T' (toggle) flip-flop, used in mode three
-
- The major functional blocks are shown in Figure 3 (in the FIGURES archive).
-
- The Mode register controls the operating mode (section »» 7.8) and the access
- mode (see section »» 7.7) of the channel. It is written at the start of the
- programming sequence. When it is written, the channel output pin usually goes
- into a defined state - see the individual mode descriptions, section »» 7.8.
-
- The Reload register can be programmed by software. The Counting register is
- reloaded from this register at certain times (depending on the operating mode).
- In modes 2 and 3, which operate as frequency dividers, this register is also
- called the divisor register.
-
- The Counting register is a down-counting 16-bit counter. Its exact behaviour
- depends on the operating mode, but generally it counts down on every CTC
- clock pulse (0.8381 us). It cannot be read directly - it is always read via
- the Latch register.
-
- The Latch register is a 16-bit software-readable transparent latch which follows
- the Counting register unless the Latch command is issued. This command makes
- the Latch register freeze, so that a stable count value can be read.
-
- The 8-bit Status Latch register is used with the read-back function when the
- channel status is latched, see section »» 7.18.
-
- The lobyte/hibyte flag is an internal flag which determines which half of the
- 16-bit Reload and Latch registers will be accessed through the 8-bit access
- port.
-
- ## 7.7 ACCESS MODES
-
- Because the I/O interface to the CTC is only eight bits wide, the CTC implements
- three Access modes which control how values are written to the Reload registers
- and read from the Latch registers.
-
- ■ Lobyte only
- ■ Hibyte only
- ■ Lobyte then hibyte (using the lobyte/hibyte flag)
-
- If lobyte only, or hibyte only, are selected, the registers are read or written
- with a single access. If lobyte/hibyte access is selected, a read or write to
- the data port will access the lobyte or hibyte of the registers, according to
- the lobyte/hibyte flag, which toggles automatically after each register access.
- In the lobyte/hibyte access mode, two 8-bit accesses are required to fully read
- the Latch register and to fully write the Reload register. Regardless of the
- access mode, the Counting register always operates as a 16-bit counter.
-
- If a channel is set for lobyte-only or hibyte-only access, when the data port
- is written, the other byte is taken to be zero. For example, for a channel set
- for lobyte-only access, writing 50 to the data port will set the reload register
- to 50, and a write of zero to the data port will set the reload register to 0,
- i.e. a divisor of 65536 in modes 2 and 3. For a channel set for hibyte-only
- access, a write of 50 to the data port will load the reload register with 12800.
-
- ## 7.8 CTC OPERATING MODES
-
- Each channel in the CTC can be independently set to one of six operating modes:
-
- ■ Mode 0: Interrupt on terminal count
- ■ Mode 1: Hardware-retriggerable one-shot
- ■ Mode 2: Rate generator
- ■ Mode 3: Square wave generator
- ■ Mode 4: Software-triggered strobe
- ■ Mode 5: Hardware-triggered strobe
-
- While reading the mode descriptions below, you may want to refer to section
- »» 7.3 and »» 7.4 for the gate and output connections for each channel.
-
- ## 7.8.1 OPERATING MODES: BEHAVIOUR COMMON TO ALL MODES
-
- When the mode word is written, all internal logic in the channel, including the
- lobyte/hibyte flag, is reset, and the output immediately goes to the initial
- state (which depends on the mode).
-
- A new value can be written into the Reload register at any time. The operating
- mode determines the exact effect that this will have, see the individual mode
- descriptions below.
-
- Loading and decrementing of the Counting register occurs on the _falling_ edge
- of the CTC clock input.
-
- The CTC samples the gate input on the _rising_ edge of the CTC clock input.
- In modes one, two, three, and five, a rising edge on the gate input sets an
- internal flip-flop, whose output is sampled on rising edges of CTC clock.
- This flip-flop is reset after its output has been sampled. Therefore the
- timing of the rising edge on gate need not be synchronised with CTC clock.
-
- In modes where falling edge on CTC clock loads the Counting register and also
- decrements it, the Counting register is not decremented on the CTC clock pulse
- which loads it. It starts decrementing on the _next_ CTC clock.
-
- The BCD/Binary flag allows BCD operation to be selected. In BCD mode, the
- Counting register operates in 4-digit binary-coded-decimal format. If the
- Counting register is zero and is decremented, it wraps around to 9999 hex.
- The Intel documentation does not describe how the chip will behave if the
- Reload register contains any digits outside the range 0-9 and I have not
- tested to find this out, as it may be implementation dependent. Also this
- feature is not normally used in the PC, and may well be non-functional on
- some workalikes (chipsets). In other words, don't use BCD mode!
-
- ## 7.8.2 OPERATING MODE ZERO: INTERRUPT ON TERMINAL COUNT
-
- When the mode word is written, the output pin goes low and the CTC waits for
- the Reload register to be loaded by software, whereupon it transfers the value
- in the Reload register into the Count register on the next falling edge of the
- CTC clock. Subsequent falling edges of CTC clock will decrement the Counting
- register _if the gate input is high_. If the gate is low, the Counting register
- will not decrement. The gate input is sampled on the rising edge of CTC clock.
-
- When the Counting register decrements from one to zero, the output goes high,
- and remains high until another Mode word is written, or another value is written
- into the Reload register. The Counting register continues to count even after
- it has decremented to zero - it wraps around to FFFF hex (9999 in BCD mode) -
- but this doesn't affect the output pin state.
-
- The Reload register may be written at any time. In two-byte access mode, when
- the first byte of the Reload register is written, counting stops and the output
- goes low. Once the Reload register is loaded, the next clock pulse will load
- the Counting register from the Reload register, and counting will resume,
- starting from the new value.
-
- See section »» 7.31 for an example of this mode being used with channel two
- and section »» 10.7 for this mode being used in PWM audio generation.
-
- ## 7.8.3 OPERATING MODE ONE: HARDWARE-RETRIGGERABLE ONE-SHOT
-
- This mode uses the gate input as a trigger. Gate is sampled on the rising edge
- of CTC clock. The trigger occurs on the rising edge of the gate input.
-
- When the mode word is written, the output pin goes high and the CTC waits for
- the Reload register to be loaded by software. It is then armed, and waits for
- a rising edge on the Gate input. Once this is detected, the next falling edge
- of CTC clock sets the output low and transfers the Reload register into the
- Counting register, and counting is enabled. On every subsequent falling edge of
- CTC clock, the Counting register decrements. When the Counting register
- decrements from one to zero, the output returns high and remains high, though
- the Counting register continues to decrement (it wraps around).
-
- During the counting period, the gate input may go low, and this will be ignored.
- A rising edge on gate during counting (a re-trigger) will cause the Reload
- register to be transferred into the Counting register on the next falling edge
- of CTC clock, as above, thus restarting the timer and extending the low-pulse
- at the output.
-
- The Reload register may be written at any time, but this will not affect the
- count in progress. This will affect the value reloaded into the Counting
- register when re-triggered.
-
- This mode is not used with channel 0 or 1, as their gate inputs are tied high.
-
- ## 7.8.4 OPERATING MODE TWO: RATE GENERATOR
-
- In mode two, the channel operates as a frequency divider. The reload register
- becomes the divisor, by which the CTC clock frequency is divided, to produce
- the output frequency. A low gate input stops the counter. When gate returns
- high, the counting register is reloaded and the count sequence begins again.
-
- When the mode word is written, the output goes high. When the Reload register
- has been written, the Reload register is transferred to the Counting register
- on the next falling edge of the CTC clock. The Counting register decrements by
- one on every falling edge of CTC clock.
-
- When the Counting register is decremented to one, the channel's output goes low.
- On the next falling edge of CTC clock the Counting register is reloaded from
- the Reload register, the output returns high, and the cycle continues.
-
- If the gate input goes low, counting stops and the output goes high immediately.
- Once the gate input has returned high, the next falling edge on CTC clock
- reloads the Counting register from the Reload register and operation continues.
-
- Programming a new value into the Reload register does not affect the count in
- progress. The next reload (due to the Counting register reaching 1 or due to
- the gate input going low then high) starts from the newly programmed value.
-
- A divisor (Reload register) value of one must _not_ be used with this mode.
-
- To summarise, the Counting register starts at the Reload register value and
- decrements down to one, then reloads. The output is low while the Counting
- register is equal to one. Thus output pulses are generated at 1.193182 MHz
- divided by the Reload register (divisor) value. The period between output
- pulses is the CTC clock period (0.8381 us) multiplied by the Reload register
- (divisor) value, and they are one CTC clock period wide.
-
- This makes mode two unsuitable for use with timer two for generating audio for
- the speaker, because the speaker cannot respond to such short pulses. For this
- reason, the 8254/8253 has operating mode three.
-
- ## 7.8.5 OPERATING MODE THREE: SQUARE WAVE GENERATOR
-
- Like mode two, mode three operates as a frequency divider. The difference is
- in the output signal. Whereas mode two produces a short pulse for every timer
- reload, mode three produces a square wave output.
-
- In this mode, the reload pulse is fed into an internal 'T' (toggle) flip-flop,
- which toggles (reverses state) on each pulse, and the output of this flip-flop
- becomes the output signal. Every time the Counting register reloads, the output
- pin toggles to the opposite state. This gives a square wave output, with equal
- high and low times (i.e. a 50% duty cycle, or 1:1 mark to space ratio). If an
- odd divisor is used, the duty cycle is not exactly 50% (as explained below).
-
- However, two reloads are needed to produce one output cycle, so the reload rate
- must be doubled to compensate for the halving action of the 'T' flip-flop. This
- is accomplished by making the Counting register decrement by two instead of by
- one for every CTC clock. So in mode three, the Counting register decrements in
- steps of two and reloads twice as fast as it would in mode two, and the twice-
- speed reload frequency is halved by the 'T' flip-flop to produce an even square
- wave output at the correct frequency.
-
- Odd divisor values are handled strangely. On every reload, the Reload register
- minus one (which will be an even value) is loaded into the Counting register.
- If the output pin is high, the chip waits until the Counting register has
- decremented to zero (not one, as would be normal), and reloads the Counting
- register on the next CTC clock after that. If the output pin is low, it reloads
- the Counting register after the Counting register reaches one, as normal. This
- makes the high pulse one CTC clock cycle wider than the low pulse, and shifts
- the output square wave's duty cycle slightly above 50%. The duty cycle error
- is only significant if the divisor value is small.
-
- The output pin goes high immediately when the mode word is written. Once the
- Reload register has been written, counting begins.
-
- If the gate input drops low, counting stops and the output pin goes high
- immediately. When the gate input has returned high, the next falling edge
- on CTC clock reloads the Counting register from the Reload register, leaving
- the output pin high, and counting resumes. If the Reload register is written
- while counting is in progress, the new value has no effect until a reload
- occurs, either due to the gate input going low then high, or due to a normal
- reload, which happens twice for every output cycle.
-
- A divisor (Reload register) value of one must _not_ be used with this mode.
-
- As well as the different output generated by the timer in modes two and three,
- there is a difference when the timer is read on-the-fly - see section »» 9.
-
- ## 7.8.6 OPERATING MODE FOUR: SOFTWARE-TRIGGERED STROBE
-
- Mode four operates as a retriggerable delay, generating a pulse when the delay
- expires. When the mode word is written, the output pin goes high. Once the
- Reload register has been written, the next falling edge of CTC clock loads the
- Counting register from the Reload register, and counting begins. When the
- Counting register decrements to zero, the output goes low for one CTC clock
- pulse then returns high. The Counting register continues to decrement,
- wrapping round to FFFF hex (or 9999 hex in BCD mode), but no more output pulses
- will occur.
-
- If the Reload register is written during counting, after the Reload register is
- fully written (both bytes, if programmed for lobyte/hibyte access), the next
- falling edge of CTC clock reloads the Counting register, retriggering the delay
- period or starting a new delay if the previous delay had expired.
-
- A low gate input disables counting but the gate input has no other effect.
-
- ## 7.8.7 OPERATING MODE FIVE: HARDWARE-TRIGGERED STROBE
-
- Mode five is a cross between mode one and mode four, using a rising edge on the
- gate input to trigger or retrigger the delay period.
-
- When the mode word is written, the output pin goes high and the CTC waits for
- the Reload register to be loaded by software. It is then armed, and waits for
- a rising edge on the Gate input. Once this is detected, the next falling edge
- of CTC clock transfers the Reload register into the Counting register, and
- counting is enabled. On every subsequent falling edge of CTC clock, the
- Counting register decrements. When the Counting register decrements to zero,
- the output goes low for one CTC clock pulse width then returns high. The
- Counting register continues to decrement, wrapping round to FFFF hex (or 9999
- hex in BCD mode), but no more output pulses will occur until the channel is
- re-triggered by another rising edge on the gate input.
-
- During the counting period, the gate input may go low, and this will be ignored.
- A rising edge on gate during counting (a re-trigger) will cause the Reload
- register to be transferred into the Counting register on the next falling edge
- of CTC clock, as above, thus restarting the timer and re-triggering the delay.
-
- The Reload register may be written at any time, but this will not affect the
- count in progress. This will affect the value reloaded into the Counting
- register when re-triggered.
-
- This mode is not used with channel 0 or 1, as their gate inputs are tied high.
-
- ## 7.9 THE 8254/8253 REGISTERS
-
- On the PC family, the 8254/8253 timer occupies four I/O addresses in the
- directly addressable I/O page, as follows:
-
- 40h Channel 0 data port (read/write)
- 41h Channel 1 data port (read/write)
- 42h Channel 2 data port (read/write)
- 43h Mode/Command register (write only - read is ignored)
-
- ## 7.9.1 THE MODE/COMMAND REGISTER
-
- The Mode/Command register at I/O address 43h is defined as follows:
-
- 7 6 5 4 3 2 1 0
- * * . . . . . . Select channel: 0 0 = Channel 0
- 0 1 = Channel 1
- 1 0 = Channel 2
- 1 1 = Read-back command (8254 only)
- (Illegal on 8253)
- (Illegal on PS/2 {JAM})
- . . * * . . . . Command/Access mode: 0 0 = Latch count value command
- 0 1 = Access mode: lobyte only
- 1 0 = Access mode: hibyte only
- 1 1 = Access mode: lobyte/hibyte
- . . . . * * * . Operating mode: 0 0 0 = Mode 0, 0 0 1 = Mode 1,
- 0 1 0 = Mode 2, 0 1 1 = Mode 3,
- 1 0 0 = Mode 4, 1 0 1 = Mode 5,
- 1 1 0 = Mode 2, 1 1 1 = Mode 3
- . . . . . . . * BCD/Binary mode: 0 = 16-bit binary, 1 = four-digit BCD
-
- You might prefer the following diagram and explanation.
-
- 7 6 5 4 3 2 1 0
- ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
- │ SC1 │ SC0 │ RL1 │ RL0 │ M2 │ M1 │ M0 │ BCD │
- └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
- │ │ │ │ │ │ │ │
- COMMAND SELECT BITS MODE SPECIFIER BITS
- │ │ │ │ │ │ │ │
- │ │ │ │ │ │ │ ├─ Binary/BCD mode
- │ │ │ │ │ │ │ │
- │ │ │ │ │ │ │ 0 = Binary
- │ │ │ │ │ │ │ 1 = BCD
- │ │ │ │ │ │ │
- │ │ │ │ ├─────┼─────┼── Mode number
- │ │ │ │ │ │ │
- │ │ │ │ 0 0 0 = Mode 0
- │ │ │ │ 0 0 1 = Mode 1
- │ │ │ │ 0 1 0 = Mode 2
- │ │ │ │ 0 1 1 = Mode 3
- │ │ │ │ 1 0 0 = Mode 4
- │ │ │ │ 1 0 1 = Mode 5
- │ │ │ │ 1 1 0 = Mode 2
- │ │ │ │ 1 1 1 = Mode 3
- │ │ │ │
- │ │ ├─────┼── Latch/Read/Write operation
- │ │ │ │
- │ │ 0 0 = Latch count value command (for read)
- │ │ 0 1 = Read/Write lobyte only
- │ │ 1 0 = Read/Write hibyte only
- │ │ 1 1 = Read/Write lobyte then hibyte
- │ │
- ├─────┼── Timer/counter number
- │ │
- 0 0 = Select channel 0
- 0 1 = Select channel 1
- 1 0 = Select channel 2
- 1 1 = Read-back command on 8254 (not allowed on 8253 and PS/2)
-
- The SC1 and SC0 (Select Channel) bits form a two-bit binary code which tells
- the CTC which of the three channels (channels 0, 1, and 2) you are talking to,
- or specifies the read-back command. As there are no 'overall' or 'master'
- operations or configurations, every write access to the mode/command register,
- except for the read-back command (see section »» 7.18), applies to one of the
- channels. These bits must always be valid on every write of the mode/command
- register, regardless of the other bits or the type of operation being performed.
-
- The RL1 and RL0 bits (Read/write/Latch) form a two-bit code which tells the CTC
- what access mode you wish to use for the selected channel, and also specify the
- Counter Latch command to the CTC. For the Read-back command, these bits have a
- special meaning (section »» 7.18). These bits also must be valid on every write
- access to the mode/command register.
-
- The M2, M1, and M0 (Mode) bits are a three-bit code which tells the selected
- channel what mode to operate in (except when the command is a Counter Latch
- command, i.e. RL1,0 = 0,0, where they are ignored, or when the command is a
- Read-back command, where they have special meanings, see section »» 7.18).
- The modes are described in section »» 7.8 and subsections. These bits must
- be valid on all mode selection commands (all writes to the mode/command
- register except when RL1,RL0 = 0,0 or when SC1,0 = 1,1).
-
- Like the Mode specification, the BCD bit must be valid on all mode selection
- commands. This bit simply specifies whether the channel will count in binary
- (the usual mode) or BCD, when it will behave as four separate cascaded 4-bit
- BCD counters. The counters always count DOWNWARDS, which can make BCD mode
- awkward to use. Also see section »» 7.8.1.
-
- ## 7.9.2 THE DATA PORTS
-
- Writing to the data ports sets the Reload register (one or two writes are used,
- according to the access mode - see section »» 7.7). Reading the ports returns
- the Latch register (lobyte, hibyte, or alternating lobyte and hibyte, depending
- on the access mode, see section »» 7.7) or the status register if a status
- read-back command has just been issued (see section »» 7.18).
-
- ## 7.9.3 ACCESSING THE REGISTERS
-
- Accessing a CTC channel involves writing one byte to the mode/command register
- at I/O address 43h, to tell the chip what you want to do, followed by reading or
- writing one, two or sometimes three bytes in succession, to or from the data
- port for the appropriate channel. This should always be done with interrupts
- disabled, because the CTC "remembers where it's up to", and will get confused
- if the normal sequence of register accesses is interrupted.
-
- Always use byte-sized I/O instructions to access these ports. In assembly, use
- OUT nn,AL or IN AL,nn (not AX). In C, use inportb() and outportb() or the
- equivalent 8-bit I/O functions or pseudofunctions for your compiler.
-
- ## 7.9.4 I/O RECOVERY DELAYS
-
- Modern CPUs operate internally and externally at very high speeds. Modern fast
- machines must be compatible with old ISA bus cards, which have slow peripheral
- devices such as serial ports, parallel ports, video and disk controllers, etc,
- accessed via the CPU's I/O space. I/O-addressed peripherals on the motherboard
- (the 8254/8253 CTC, the 8237 DMA controllers, the 8259 interrupt controllers,
- the real time clock, etc) are also slow by the standards of a modern CPU.
-
- On these fast machines, whenever the CPU makes an access to an I/O device (via
- the IN and OUT instructions and variants), hardware on the motherboard must
- slow down the access, in order to guarantee that the timing requirements of the
- slow peripheral are not violated (i.e. to give the peripheral enough time to
- provide or accept the data correctly and prepare for the next data transfer).
-
- There are two parameters of interest - the access time, and the recovery time.
- These times are in the order of several hundred nanoseconds, but depend on the
- motherboard and peripheral device in question. These times do not apply to
- memory accesses, which are cached and are much faster and use few wait states.
-
- The following diagram is a _simplified_ representation of what happens when the
- CPU executes two I/O read instructions (for example, "in al,42h / in al,40h").
-
- ┌ Valid ┌─42h────────────┐ ┌─40h────────────┐
- ADDRESS │ │ │ │ │
- └ Not valid ───┘ └────────┘ └──...
- : : : :
- ┌ Valid ┌─IOR────────────┐ ┌─IOR────────────┐
- CONTROL │ │ │ │ │
- └ Not valid ───┘ └────────┘ └───...
- ├───Tacc───┤ ├──Trec──┼───Tacc───┤ :
- ┌ Valid : ┌─────┐ : ┌─────┐
- DATA │ : │ │ : │ │
- └ Not valid ──────────────┘ └───────────────────┘ └────...
- : : : : : : : :
- NOTES time--> a b c d e f g h
-
- At point 'a' the CPU makes an I/O read request. The address and control buses
- become valid. The address decoding logic sees that the address is in the range
- 40h-43h and selects the CTC. From this point, the CTC takes Tacc (the access
- time) to get itself ready and present the data on the data bus. At point 'b'
- the CTC has made the data available on the data bus. At 'c' the CPU reads the
- data from the data bus. At point 'd' the cycle is complete and the address and
- control buses go inactive. The CPU transfers the data into the AL register.
- At point 'e' the CPU generates a second access cycle just like the first. The
- peripheral also requires a certain amount of time to elapse between points 'd'
- and 'e' - this is called the peripheral's recovery time.
-
- The access time is the time required by the peripheral to accept data correctly
- (for an I/O write) or provide data correctly (for an I/O read). It is required
- on every access to an I/O device. The recovery time is the time required by
- the peripheral _after_ an I/O access, before the peripheral is ready to receive
- another I/O request.
-
- An analogy would be a little old lady in a car at an intersection. When the
- light changes, she fumbles around looking for the handbrake, then she tries to
- remember which pedal to push to go faster. Then she finally takes off. This
- is like the access time requirement. Then at the next intersection, she has to
- slow down and stop, and get ready for the lights to change again. This takes
- time. If the lights change before she has stopped and finished getting ready,
- she will do something stupid like crunching the gearbox or driving into a tree.
- This requirement is the recovery time.
-
- On slow motherboards (the old PC and XT, and probably most 286-based boards),
- the access time and recovery time are both guaranteed to be met because the
- CPU's bus interface is fairly slow, and comparatively fast peripheral devices
- are used (the 8254's recovery time is 165 or 200 ns, compared to the old 8253's
- recovery time of 1000 ns!).
-
- On fast motherboards, the access time is assured because the chipset on the
- motherboard inserts I/O wait states, but on some fast motherboards, notably
- some 286 and early 386 motherboards, the _recovery_ time is not guaranteed.
- The motherboard says "I have to wait until this peripheral is ready, but after
- the access is complete, I don't care". With these boards, two back-to-back I/O
- accesses to the _same_ peripheral (such as the sequence shown in the diagram
- above) will cause the second access to be ignored or misinterpreted by the
- peripheral.
-
- This design misfeature of some 286 and 386 motherboards is probably the result
- of a design compromise. From the point of view of a chipset designer there are
- several ways to deal with I/O recovery time requirements -
-
- 1. Enforce a recovery time after every I/O access, or
- 2. Enforce a recovery time between any back-to-back I/O accesses, or
- 3. Enforce a recovery time between back-to-back I/O accesses to the
- _same_ peripheral, or
- 4. Never enforce a recovery time after an I/O access.
-
- The first alternative would slow the machine unduly, because the recovery time
- would be enforced even if the next accesses were memory accesses (which would
- not affect the I/O-addressed peripheral). The second option is complicated to
- implement (though modern motherboards use this method, I believe). The third
- option is even more complicated. The fourth is the simplest approach, but it
- means that back-to-back accesses to the same peripheral will violate that
- peripheral's recovery time requirements.
-
- To support these motherboards, programmers would insert the famous "jmp short
- $+2" sequence into their code between back-to-back I/O accesses to the same
- device. The instruction is effectively a NOP (no-operation) instruction but
- it has an extra delaying effect because it clears the processor's instruction
- prefetch queue on the 286 and 386, requiring an external bus access, which must
- wait for the I/O access cycle to complete.
-
- Modern motherboards detect back-to-back I/O accesses, and insert wait states to
- ensure that recovery times are not violated, so there is no need to use this
- trick with them, but to support older systems, you may wish to do so. The
- sample code and programs in this document do use the "jmp short $+2" trick,
- because I am in the habit of using it.
-
- In C, you could use the inline assembler feature of most compilers, but Michael
- Mauch (mauch@uni-duisburg.de) advises that the optimiser may optimise out this
- instruction, so he suggests (for Borland C++ 3.1 and 4.0), __emit__(0xEB,0x00);
- which is not optimised out. You could set up a macro, e.g. #define breather
- __emit__(0xEB,0x00). In Turbo Pascal, you could use the appropriate directive
- to emit the two-byte instruction. The object code is $EB/$00. If anyone knows
- a better or more generic way to implement this in C and/or Pascal, please tell
- me. (*)
-
- An alternative method to the "jmp short $+2" method is to insert an access to
- another I/O location. This enforces another access time delay, which should
- cover the recovery time requirements of the first device. You could then
- interleave accesses to the device you are interested in, with accesses to the
- 'dummy' device. Apparently it is common practice to use "in al,61h" as the
- dummy instruction for this purpose. Port 61h is Port B (see section »» 7.5)
- and it can be read at any time with no unusual side-effects, so is ideal for
- this purpose, except that the IN instruction destroys AL, which is often
- inconvenient. An OUT instruction is more covenient but there is no port that
- can safely have any value OUTed to it. In the quoted message below, Bob Smith
- (bobs@access.digex.net) mentions that some IBM BIOSes use an OUT to port 4Fh
- (an unused I/O address) to insert delays.
-
- In an article in Usenet newsgroup comp.lang.asm.x86 in December 1995, Bob Smith
- (bobs@access.digex.net) posted the following interesting information:
-
- > The reason there is a short jump to the next instruction is certainly, as
- > [most people would say], that some I/O devices need more recovery time.
- > Moreover, the 386 processor treats a flush of the prefetch queue specially
- > with respect to I/O operations. The problem is that that trick doesn't work
- > any more! Quoting from the (now out-of-print) "i486 Microprocessor Data
- > Sheet" (Intel order #240440-001):
- >
- > "6.3.1 WRITE BUFFERS AND I/O CYCLES
- >
- > "Input/Output (I/O) cycles must be handled in a different manner by the write
- > buffers. I/O reads are never reordered in front of buffered memory writes.
- > This ensures that the 486 microprocessor will update all memory locations
- > before reading status from an I/O device.
- >
- > "The 486 microprocessor never buffers single I/O writes. When processing an
- > OUT instruction, internal execution stops until the I/O write actually
- > completes on the external bus. This allows time for the external system to
- > drive an invalidate into the 486 microprocessor or to mask interrupts before
- > the processor progresses to the instruction following OUT. Repeated OUT
- > instructions will be buffered.
- >
- > "I/O device recovery time must be handled slightly differently by the 486
- > microprocessor than with the 386 microprocessor. I/O device back-to-back
- > write recovery times could be guaranteed by the 386 microprocessor by
- > inserting a jump to the next instruction in the code that writes to the
- > device. The jump forces the 386 microprocessor to generate a prefetch bus
- > cycle which can't begin until the I/O write completes.
- >
- > "Inserting a jump to the next write will not work with the 486 microprocessor
- > because the prefetch could be satisfied by the on-chip cache. A read cycle
- > must be explicitly generated to a non-cacheable location in memory to
- > guarantee that a read bus cycle is performed. This read will not be allowed
- > to proceed to the bus until after the I/O write has completed because I/O
- > writes are not buffered. The I/O device will have time to recover to accept
- > another write during the read cycle."
- >
- > FWIW, I have seen some BIOSes (in IBM systems) use an OUT (of any value) to
- > I/O port 4Fh (an otherwise unused port) in order to provide the needed
- > synchronization.
-
- Thanks Bob for that information. I believe Glen Blankenship (obother@netcom.
- com) also quoted the same information in a separate message.
-
- ## 7.10 PROGRAMMING THE MODE AND RELOAD REGISTER
-
- Until initialised, all channels are in an undefined state. The BIOS POST sets
- the operating modes for all channels. Channels can be programmed in any order.
- For any particular channel, the Mode register must be programmed first. Once
- the mode is set, one or two bytes (depending on the access mode - see section
- »» 7.7) are written into the data port for that channel; these are loaded into
- the Reload register. The channel is then initialised and begins operating
- according to the programmed mode.
-
- To program the mode and reload value for a CTC channel, issue a command byte of
- ccaammmb binary, where 'cc' = channel number, 'aa' = access mode, 'mmm' = mode,
- 'b' = BCD/binary selection (see section »» 7.9.1 for the bit definitions) then
- write the lobyte, or the hibyte, or lobyte then hibyte (depending on the access
- mode) of the reload value to the data port of the selected channel.
-
- Note - a reload value of 1 should NOT be used in modes two and three. Also,
- in these modes, low reload values will give very high output frequencies, and
- are not normally used with channel zero because the tick rate would be too high.
-
- The Reload register may be reprogrammed at any time, just by writing the lobyte,
- hibyte, or lobyte then hibyte (depending on the access mode), to the data port.
- See section »» 7.7 and subsections for details.
-
- ## 7.11 EFFECT OF REPROGRAMMING CHANNEL ZERO ON THE TIMER TICK INTERRUPT
-
- The system time is maintained using the timer tick interrupt, and reprogramming
- the mode of channel zero will reset the channel. When the mode word is written,
- the channel zero output pin of the CTC goes high immediately. If it was already
- high, no interrupt is generated. If it was low, an interrupt _is_ generated,
- causing the BIOS timer tick count variable to be incremented incorrectly. If
- CTC channel zero was previously programmed for mode 2, its output would already
- be high, and no extra interrupt would be generated.
-
- This should not be done continuously in an application unless you restore the
- correct DOS time at termination. Normally it is sufficient to reprogram channel
- zero at the start of your program, and leave it in that mode until finished.
- This does cause a slight jump in the time, but as it only happens once on every
- run of the program, it is not really worth worrying about.
-
- If you want to be more careful, you can wait until the timer tick interrupt
- occurs, and reprogram channel zero immediately after the interrupt has occurred.
- You can detect the interrupt by watching the BIOS tick count variable until it
- changes (only the loword need be monitored, as it always changes on each tick
- interrupt). When the interrupt has just occurred, the channel zero output pin
- will be high, so reprogramming the channel will not generate an interrupt, and
- the Counting register will be near the start of its 54.9254 ms cycle.
-
- A similar approach should be used when terminating the program, after channel
- zero has been reprogrammed with a smaller divisor to give a faster tick rate.
- Assuming your int 8 handler chains to the BIOS int 8 handler every 54.9254 ms,
- wait until the tick count changes (i.e. your int 8 handler has called the BIOS
- int 8 handler), then reprogram channel zero with the default parameters (mode
- 3 or 2, divisor of 65536).
-
- These considerations do not apply when the Reload register is loaded without a
- mode initialisation command written to the Mode/Command register, as done with
- the dynamic interrupt rate technique (see section »» 8.6).
-
- ## 7.12 SAMPLE PROGRAM: PROGRAMMING THE MODE AND RELOAD VALUE
-
- This function programs the operating mode and the reload value (the divisor in
- modes two and three) for a specified channel. If you use channel zero in a
- non-standard setup, you should restore it to its normal mode and divisor (mode
- two or three, with a divisor of 65536) when you've finished using it. See
- section »» 5 for details of how to intercept the Ctrl-C and Critical Error
- vectors, so that you can restore the normal mode at program termination even
- if the program is terminated by Ctrl-Break or Ctrl-C being pressed by the user,
- or due to a critical error.
-
- The init_channel() function accepts a channel number, a reload value which will
- be in the range 0 to 65535 (in modes two and three, a zero divisor gives
- division by 65536, and a divisor of one should not be used), and an operating
- mode number. The access mode is not provided as a parameter - the function
- always programs the channel for lobyte/hibyte access.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #5
- Program the operating mode and reload value for a CTC channel
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE5.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample5.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <ctype.h> /* Needed for toupper() */
- #include <process.h>
- #include <stdio.h> /* Pass go, add printf(), program is 8K already :-) */
- #include <stdlib.h> /* Needed for atoi() */
-
- char *accessmodes[] = {
- "",
- "lobyte-only",
- "hibyte-only",
- "lobyte/hibyte"
- };
-
- void init_channel(unsigned int channum, unsigned int accessmode,
- unsigned int mode, unsigned int reload) {
- if (channum > 2 || accessmode < 1 || accessmode > 3)
- return;
- asm pushf; /* Preserve interrupt flag */
- asm cli;
- outportb(0x43, (channum << 6) + (accessmode << 4) + ((mode & 0x07) << 1)); /* Mode */
- if (accessmode & 1)
- outportb(0x40 + channum, reload & 0xFF); /* Reload reg lobyte */
- if (accessmode & 2)
- outportb(0x40 + channum, (reload >> 8) & 0xFF); /* Reload reg hibyte */
- asm popf; /* Restore interrupt flag */
- return;
- }
-
- void usage(void) {
- printf("Usage: SAMPLE5 <channel> <accessmode> <operatingmode> <reload>\n\n");
- printf("\tchannel is 0, 1, or 2\n");
- printf("\taccessmode may be:\n");
- printf("\t\tL = Lobyte only\n");
- printf("\t\tH = Hibyte only\n");
- printf("\t\tW = Lobyte/hibyte (16-bit)\n");
- printf("\toperatingmode may be:\n");
- printf("\t\t0 = Interrupt on terminal count\n");
- printf("\t\t1 = Hardware-retriggerable one-shot\n");
- printf("\t\t2 = Rate generator\n");
- printf("\t\t3 = Square wave generator\n");
- printf("\t\t4 = Software-triggered strobe\n");
- printf("\t\t5 = Hardware-triggered strobe\n");
- printf("\treload is an unsigned 16-bit value, use zero for divide-by-65536\n");
- return;
- }
-
- void main(unsigned int argc, char * argv[]) {
- unsigned int channum, accessmode, mode, reload;
-
- printf("Sample program #5 - Set the mode and reload value for a CTC channel\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
-
- if (argc < 5) {
- usage();
- exit(1);
- }
- channum = argv[1][0] - '0';
- switch (toupper(argv[2][0])) {
- case 'L':
- accessmode = 1;
- break;
- case 'H':
- accessmode = 2;
- break;
- case 'W':
- accessmode = 3;
- break;
- default:
- usage();
- exit(1);
- }
- mode = argv[3][0] - '0';
- if (channum > 2 || mode > 5) {
- usage();
- exit(1);
- }
- reload = atoi(argv[4]);
- printf("Setting CTC channel %d for %s access, mode %d, with " \
- "reload value %ld\n", channum, accessmodes[accessmode],
- mode, (long)(reload ? reload : 65536L));
- init_channel(channum, accessmode, mode, reload);
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.13 READING THE RELOAD REGISTER
-
- It is not possible to read the Reload register contents. In modes two and
- three, it may be possible to infer the reload register value using clever
- techniques, but I don't believe there is any good reason to pursue this.
-
- ## 7.14 READING THE COUNTING REGISTER
-
- Reading the Counting register on-the-fly gives you a fairly accurate time value
- with a resolution of 0.8381 us for calculating elapsed time or timestamping
- internal or external events. You do not actually read the Counting register
- directly, it is read via the Latch register, which follows the Counting register
- value unless it is latched via the latch command.
-
- You can read the Counting register by making one or two (depending on the access
- mode) reads from the data port of the appropriate channel, however this value is
- not latched, and is not stable. In lobyte/hibyte access mode, there is a delay
- between reading the lobyte and hibyte, so the lobyte and hibyte don't correspond
- to the same instant in time, and you may read an incorrect value. This problem
- does not occur if the access mode is lobyte-only, or hibyte-only.
-
- {JAM} Some CTC hardware implementations do not buffer the counter properly, so
- if the Counting register is read at the instant it is changing value, you may
- read the counter part-way through the 'ripple-through', i.e. some low-order bits
- may have decremented but high-order bits may not have decremented yet.
- Therefore, even in lobyte-only or hibyte-only mode, the Counting register cannot
- be read reliably in this way.
-
- The CTC provides a latch command to avoid these problems. When the latch
- command is issued, the Latch register freezes, and the Counting register
- continues to count. Thus the Latch register contains a stable count which
- can be read via the data ports in the normal way. Once the appropriate number
- of bytes (one or two, depending on the access mode) have been read, the Latch
- register unlatches and resumes following the Counting register.
-
- ## 7.15 THE LATCH COMMAND
-
- To latch a channel, write a latch command byte to the Mode/Command register.
- The latch command byte is cc000000 binary, where 'cc' is the channel number.
- Then you can read the latched count from the data register for that channel.
- The Latch register remains latched until it has been fully read, or until the
- counter is reprogrammed with a new mode word. The latched value must be read
- before any other operation is performed on the channel, except initialising
- the channel with a new mode word.
-
- {JAM} Latching the count in progress should not affect the Counting register
- but when several machines were tested, they tended to occasionally miss a CTC
- clock, i.e. fail to decrement, if latch commands were being issued. This was
- much more pronounced on an Epson 386SX/20 PLUS, which would miss roughly one
- clock for every two latch commands issued! This seems to be an isolated
- example of bad hardware design, but is still disturbing.
-
- The channel can also be latched via the read-back command (section »» 7.18).
-
- The meaning of the value you read depends on the mode of the channel. The
- meaning of the count in modes two and three are described in sections »» 7.15.1
- and »» 7.15.2.
-
- ## 7.15.1 MEANING OF COUNT VALUE IN MODE TWO
-
- In mode two, the value will be in the range of 1 to the divisor register value.
- It will start at the divisor register value, and decrement down to 1. When it
- would decrement to zero, it instead reloads to the divisor register value. For
- example if the divisor was 5, the count sequence would be 5, 4, 3, 2, 1, 5, 4...
- If the divisor is 0 (i.e. 65536), the sequence is 0, 65535, 65534, ... 2, 1, 0,
- 65535...
-
- For channel zero, a rising edge on the output pin triggers the timer tick
- interrupt at the instant that the channel reloads its Counting register from
- the Reload register.
-
- {JAM} On PS/2 machines, if the latch command is issued at the instant when the
- Counting register changes from 1 to the reload value, occasionally the read will
- yield a zero, even if the Reload register does not contain zero. In other
- words, if the Reload register is 20, the count sequence would be 5, 4, 3, 2,
- 1, 20, 19, 18... At the instant between the 1 and the 20, the timer does
- actually decrement to zero, and sometimes a zero will be read, even though
- zero is not in the valid counting sequence.
-
- If the divisor is 65536, the above problem mentioned by {JAM} does not occur.
- In other cases, you could work around the problem by specifically checking for
- a value of zero and substituting the reload value.
-
- In mode two, if you are using a divisor of 65536 (the normal value for channel
- zero), you can convert the down-counting value into an up-counting value by
- performing a 16-bit negation, i.e. up_count = 0 - read_count0(); or neg ax (or
- whichever register contains the count). This will give a 16-bit value which
- increases from 0 to 65535 then back to 0 again.
-
- If the divisor is not 65536, just subtract the count value from the divisor
- value to get an up-counting value which will increase from 0 to divisor minus
- one, then back to 0 again. See the above problem noted by {JAM}.
-
- ## 7.15.2 MEANING OF COUNT VALUE IN MODE THREE
-
- Refer to section »» 7.8.5 for a description of the operation of mode three.
- The raw count will always be an even value, because the Counting register
- decrements in steps of two instead of steps of one. The behaviour with an
- even divisor is easiest to describe, so I will assume that the divisor value
- is even. In this case, the count register counts down from the divisor value,
- in steps of two, until it reaches two, then reloads to the divisor value on the
- next CTC clock. The output latch toggles state at this moment. For example,
- if the divisor is 6, the count sequence would be 6, 4, 2, 6, 4, 2, 6, 4, 2...
- with the output latch toggling at the transition between each '2' and '6'.
-
- In this mode, to generate a full timestamp, you need to latch and read the
- Counting register _and_ the output pin state, so you know whether the channel
- is on its first or second countdown. The timer tick interrupt only occurs on
- the rising edge of the output of the T flip-flop, at the end of every second
- countdown (if we define the first countdown as when the output of the T flip-
- flop is high, and the second countdown as when its output is low). The read-
- back function is useful for this (it allows the count register and the output
- state to be latched and read by software), and is described in section »» 7.18.
-
- Mode two is more suitable than mode three for timestamping or timing functions,
- because the Counting register behaves sensibly and there is no need to know
- whether it is on the first or second countdown.
-
- On machines that support readback (all AT-class machines except the PS/2, see
- section »» 7.24.2), the count can therefore be read on-the-fly in mode three.
- See section »» 7.20 for details.
-
- See also section »» 7.15 for {JAM}'s comments on loss of CTC clocks when the
- channel is latched or read-back.
-
- ## 7.16 SAMPLE CODE: READING THE COUNT IN MODE TWO
-
- This function latches, reads, and returns the current Counting register contents
- of CTC channel zero. Remember that the Counting register counts downwards.
-
- This function assumes that CTC channel zero is operating in mode two with a
- divisor of 65536. See section »» 7.10 and »» 7.12 for sample code to set the
- mode and divisor.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Function to latch and read the Counting register of CTC channel zero, assuming
- that the channel is set to operate in mode two with a divisor of 65536.
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
- */
-
- unsigned int read_channel0_mode2(void) {
- unsigned int cv;
- asm pushf; /* Preserve interrupt flag */
- asm cli;
- outportb(0x43, 0); /* Latch the count register */
- cv = inportb(0x40); /* Lobyte of count */
- cv += inportb(0x40) << 8; /* Hibyte of count */
- asm popf; /* Restore interrupt flag */
- return cv; /* Return down-counter */
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.17 THE LOBYTE/HIBYTE FLAG
-
- Each timer channel has an internal flag which keeps track of whether the lobyte
- or the hibyte of the count should be provided when the data port is read. Each
- time the data port is read, this flag toggles state (unless the channel was
- programmed for hibyte-only or lobyte-only access, i.e. bits 5 and 4 were 0,1
- or 1,0 when it was initialised).
-
- After programming a timer channel, the flag is clear, and reading the data port
- will yield the lobyte, then the hibyte, then the lobyte, then the hibyte, etc.
- But if some other badly-behaved software reads the data port only once (or any
- odd number of times), the flag would be set, and you would read the hibyte
- first, then the lobyte, so you would be out of sync with the counter. There is
- no processor-accessible flag to tell you whether you are reading the lobyte or
- the hibyte. Issuing a latch command doesn't affect the lobyte/hibyte flag,
- either, unfortunately.
-
- This is why it's essential to disable interrupts while accessing the CTC, and
- always read or write BOTH bytes (unless the channel is programmed for lobyte-
- only or hibyte-only access).
-
- My experience has been that if you initialise the counter in your program
- (initialising it clears the lobyte/hibyte flag), it will stay synchronised, and
- there is no need to worry about the flag at all. If anyone has found otherwise,
- please tell me about it. (*)
-
- See section »» 7.27 for a program which attempts to determine the lobyte/hibyte
- flag state (among other things).
-
- ## 7.18 THE READ-BACK COMMAND
-
- The read-back command word is written to the mode/command register. Bits 7 and
- 6 of the command word (normally the counter select bits) are both '1'.
- Read-back is not supported on the 8253 (PCs and XTs); it was added with the
- 8254 (AT and later). However, {JAM} says all AT documentation states that
- this bit combination is reserved and, alas, the PS/2 LSI integration of the
- CTC does not implement the read-back command - on a PS/2 the read-back command
- is ignored. {JAM} has tested IBM ValuePoints and they are alright. It is
- just the PS/2 that does not support read-back (see section »» 7.24.2).
-
- A read-back command is specified by writing a value to the mode/command
- register as follows:
-
- 7 6 5 4 3 2 1 0
- 1 1 . . . . . . (Specify read-back command)
- . . * . . . . . Latch count flag: 0 = Yes, 1 = No
- . . . * . . . . Latch status flag: 0 = Yes, 1 = No
- . . . . * . . . Read-back timer channel 2: 1 = Yes, 0 = No
- . . . . . * . . Read-back timer channel 1: 1 = Yes, 0 = No
- . . . . . . * . Read-back timer channel 0: 1 = Yes, 0 = No
- . . . . . . . 0 (Reserved for future expansion)
-
- Command word bits 3, 2, and 1 enable read-back for timer channels 2, 1, and 0
- respectively, thus any combination of the three channels can be selected for
- read-back with one command word. Bits 5 and 4 enable the two types of
- read-back. Important - Setting these bits to _zero_ enables the function.
-
- Bit 5 specifies latching the count value. This is the same as issuing a counter
- latch command (cc000000 binary), but several counters can be latched at the same
- time, depending on which counters are enabled by bits 3, 2, and 1 of the
- read-back command word.
-
- Bit 4 specifies latching the channel status. If this function is enabled (by
- setting the bit to 0), the next read of the data register for that channel will
- yield a status read-back byte, which is defined as follows:
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . Output pin state
- . * . . . . . . Null Count flag
- . . * * . . . . Access mode as specified at initialisation
- . . . . * * * . Operating mode as specified at initialisation
- . . . . . . . * BCD flag as specified at initialisation
-
- The bottom six bits return the values programmed into the channel when it was
- last initialised by a write of a mode word. Bits 7 and 6 relate to real-time
- events.
-
- Bit 7 indicates the actual state of the output pin of the timer chip at the
- moment that the read-back command was issued, and bit 6 indicates whether a
- newly-programmed divisor value has been loaded into the Counting register yet
- (if clear) or the channel is still waiting for a trigger signal or for the
- Counting register to count down to zero before a newly programmed Reload value
- is loaded into the Counting register (if set).
-
- The bit is set upon a mode or Reload value write to the channel, and cleared
- when the Reload value is loaded into the Counting register.
-
- ## 7.19 SAMPLE CODE: READ-BACK
-
- This function performs a full read-back on a specified CTC channel and fills in
- a readback_data structure with the count and the read-back status byte.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Function to read-back a counter/timer channel count and status
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- typedef struct {
- unsigned int count;
- unsigned char status;
- } readback_data;
-
- void readback_channel(unsigned int channum, readback_data * rbdp) {
- if (channum < 3) {
- asm pushf; /* Preserve interrupt flag */
- asm cli; /* Disable interrupts */
- outportb(0x43, 0xC0 + (2 << channum)); /* Latch count, status */
- rbdp->status = inportb(0x40 + channum); /* Get status */
- rbdp->count = inportb(0x40 + channum); /* Get count lobyte */
- rbdp->count += inportb(0x40 + channum) << 8; /* Get count hibyte */
- asm popf; /* Restore interrupt flag */
- return;
- }
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.20 READING THE COUNT IN MODE THREE (8254 ONLY)
-
- Reading the count on-the-fly to get an absolute timestamp in mode three is
- more awkward than reading the count in mode two, and has a higher overhead.
-
- As far as I know, there is no reason why your program should not program CTC
- channel 0 to operate in mode 2 and leave mode 2 in effect when your program
- exits or is terminated (modern BIOSes set mode 2 as the default mode anyway,
- see section »» 7.4.2), so there should be no requirement to be able to read
- the count in mode 3. However, if the CTC is an 8254 (not an 8253 or a PS/2
- CTC) it is possible to read the count in mode 3, so I will describe how this
- is done.
-
- The function presented in the section »» 7.21 is the result of some testing
- and experimentation. I have found it to be reliable on all of the machines I
- was able to test with, but if you have trouble with it, let me know. (*)
-
- The basis of reading the count in mode three is to read the count value, and
- also read the output pin state, then combine them. The count register counts
- down in sequence 0, 65534, 65532, 65530 ... 8, 6, 2, 0, 65534, 65532... with the
- output pin state toggling on each transition from 2 to 0. The rising edge of
- the output pin will initiate a timer tick interrupt, therefore I regard this
- as starting the count sequence, so when the output pin is low, the counter is
- on its second pass. See sections »» 7.8.5 and »» 7.15.2 for more details.
-
- We could use a read-back command to read the count and the output pin status,
- and derive an up-count combined value as:
-
- up_count = ((0 - actual_count) / 2) + (output_state ? 0 : 0x8000);
-
- This will work, and is reliable on some machines, but on other machines, the
- output pin state is occasionally read incorrectly, probably due to delays in
- the logic of the timer chip. So, I had to modify the routine to read the output
- pin state, read the count, read the output pin state again, then determine the
- true count in progress.
-
- The logic here is as follows: If the second output state is the same as the
- first (this is nearly always the case), then the output state and the count are
- both valid. If the output states are different, then a counter reload has
- occurred during the reading process, so use the count value to determine whether
- the count was latched just before, or just after, the output changed state.
- If the count value (after converting to an up-count) is small, then it was read
- just after the output changed state, so use the second output state. If the
- count is large, then it was read just before the output changed state, so the
- first output state is applicable.
-
- Now that I have the correct output state, the equivalent up-count value can be
- calculated using the above formula. This yields a 16-bit up-counting value
- which corresponds to the negative of the equivalent raw count in mode two.
-
- Needless to say, the part of the routine that talks directly to the timer chip
- operates with interrupts locked out.
-
- ## 7.21 SAMPLE CODE: READING THE COUNT IN MODE THREE
-
- This function latches, reads, and returns the current effective count value for
- timer channel zero, converted to a 16-bit up-counting value. It works with
- 8254 CTCs and fully compatible ASICs, but does not work with 8253s or on PS/2
- machines.
-
- This function assumes that CTC channel zero is operating in mode three with a
- divisor of 65536. This USED TO BE the default mode set up by the BIOS, but
- mode 2 is the default used by modern 486 BIOSes that I have seen. See section
- »» 7.4.2 for details. The function also assumes that channel zero is set for
- lobyte/hibyte access (bits b5,4 = 1,1 in control register at initialisation)
- and that the lobyte/hibyte flag is correctly synchronised (see sections »» 7.7
- and »» 7.17.
-
- See section »» 7.12 for sample code to set the mode and divisor.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Function to read the count register (down-counter) of timer channel zero,
- assuming that the timer is in mode three, with a divisor of 65536.
- Returns the count in up-counter format. Requires an 8254 timer chip.
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
- */
-
- unsigned int read_timer0_mode3(void) {
- unsigned char st1, st2; /* Status read-back values */
- unsigned int cv; /* Count value */
- disable(); /* No ints please - can use asm cli */
- outportb(0x43, 0xE2); /* Latch and read back status byte */
- st1 = inportb(0x40); /* Read status byte */
- outportb(0x43, 0x00); /* Latch count for timer 0 */
- cv = inportb(0x40); /* Lobyte of count */
- cv += inportb(0x40) << 8; /* Hibyte of count */
- cv = (0 - cv) >> 1; /* Convert to up-count, 0-32767 */
- outportb(0x43, 0xE2); /* Latch and read back status byte */
- st2 = inportb(0x40); /* Read status byte */
- enable(); /* Ints back on - can use asm sti */
- if ((st1 ^ st2) & 0x80) /* If output pin changed state... */
- if (cv < 0x4000) /* If reload just occurred... */
- st1 ^= 0x80; /* Use newer output pin status */
- if ((st1 & 0x80) == 0) /* If on second countdown... */
- cv |= 0x8000; /* Set b15 */
- return cv; /* Return as up-counter */
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.22 SAMPLE CODE: OPTIMISED MODE THREE COUNT READING FUNCTION
-
- The following function reads the count register of CTC channel zero assuming
- that CTC channel zero is operating in mode three with a divisor of 65536 and
- is set for lobyte/hibyte access, and the lobyte/hibyte flag is correctly
- synchronised.
-
- The value is returned in up-counting format, in the range 0-65535, and is
- the effective value that would be read from the counter in mode two using a
- raw read, except that the counting direction is reversed (the value returned
- by this function is an up-counter, the raw value is a down-counter).
-
- -------------------------------- snip snip snip --------------------------------
- ; Function to read the count register (down-counter) of CTC channel zero,
- ; assuming that the channel is in mode three, with a divisor of 65536.
- ; Returns the count in up-counter format. Requires an 8254 timer chip.
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- _read_timer0_mode3 PROC near ; or FAR for far code model
- ; unsigned int read_timer0_mode3(void);
- pushf ; Keep interrupt flag
- mov al,11100010b ; Latch and read back status byte only
- cli ; Lock out interrupts
- out 43h,al ; Send it
- jmp SHORT $+2 ; Delay
- in al,40h ; Get status byte
- mov ah,al ; To AH
- jmp SHORT $+2 ; Delay
- mov al,00000000b ; Latch count for timer 0
- out 43h,al ; Send it
- jmp SHORT $+2 ; Delay
- in al,40h ; Get lobyte of count
- mov dl,al ; Save in DL
- jmp SHORT $+2 ; Delay
- in al,40h ; Get hibyte of count
- mov dh,al ; Save in DH
- jmp SHORT $+2 ; Delay
- mov al,11100010b ; Latch and read back status byte again
- out 43h,al ; Send it
- jmp SHORT $+2 ; Delay
- in al,40h ; Get status byte
- popf ; Restore interrupt flag
- neg dx ; Convert to ascending count
-
- xor al,ah ; Did the output change?
- jns GotCount ; If not, no problemo
-
- test dh,dh ; Was count high or low?
- js GotCount ; If count was about to carry, keep old
- not ah ; If count just carried, change output
-
- GotCount: shl ah,1 ; Get output pin status to CF
- cmc ; Pin high = count 0-32767
- rcr dx,1 ; Pin low = count 32768-65535
- ret ; Return 16-bit ascending count in DX
- _read_timer0_mode3 ENDP
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.23 SAMPLE PROGRAM: MANIPULATE THE CTC AND PORT B
-
- The following program is a command driven utility that manipulates the CTC and
- the Port B hardware. It lets you send commands to the mode/command register,
- read and write the data registers in single-byte or lobyte/hibyte modes, set
- and display the Timer 2 Gate and Speaker Gate signals, and read the Timer 2
- output on port B or C (see section »» 7.5). It has a simple help summary which
- is displayed when '?' is entered at the prompt. The program performs minimal
- error checking and is not intended to be bulletproof. You may find it useful
- for testing some subtle details of the CTC's operation.
-
- Parameters to a command must be separated from the command name by one or more
- spaces or tabs. Commands on the same line may be separated by semicolons (;)
- and the whole command line will be executed with interrupts locked out. Result
- text is stored in an internal buffer and displayed once the command line has
- been fully processed.
-
- Numeric parameters are assumed to be binary by default. To specify a hex value,
- prefix the hex digits with 'x' (e.g. 'xFEDC'). To specify a decimal value,
- prefix the digits with 'd' (e.g. 'd12345').
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #6
- Utility to manipulate the CTC
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE6.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample6.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <ctype.h> /* For tolower() */
- #include <dos.h> /* For inportb() and outportb() */
- #include <io.h> /* For read() and write() */
- #include <stdio.h> /* For printf() */
- #include <stdlib.h> /* For exit() */
- #include <string.h> /* For strlen() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDIN 0
-
- #define LINELEN 120 /* Line length limit */
-
- static unsigned int eval_ok;
-
- static char resulttext[10240]; /* Buffer for result text */
- static char * resulttextp;
-
- unsigned int eval_value(char * s) {
- unsigned int p, v;
- char c;
- p = v = 0;
- eval_ok = TRUE;
- if (s[0] == 'd') { /* Decimal value */
- ++p;
- while ((c = s[p++]) > ' ') {
- v *= 10;
- if ((c >= '0') && (c <= '9'))
- v += (c - '0');
- else
- return (eval_ok = FALSE);
- }
- return v;
- }
- if (s[0] == 'x') { /* Hex value */
- ++p;
- while ((c = s[p++]) > ' ') {
- v <<= 4;
- if ((c >= '0') && (c <= '9')) {
- v += (c - '0');
- continue;
- }
- if ((c >= 'a') && (c <= 'f')) {
- v += (c - 'a' + 10);
- continue;
- }
- return (eval_ok = FALSE);
- }
- return v;
- }
- while ((c = s[p++]) > ' ') { /* Binary value - default */
- v <<= 1;
- if ((c == '0') || (c == '1'))
- v += (c - '0');
- else
- return (eval_ok = FALSE);
- }
- return v;
- }
-
- void rw_reg(unsigned int is16, unsigned int chan, char * parms) {
- unsigned int ioadr, v;
- ioadr = 0x40 + chan;
- if (parms[0]) {
- v = eval_value(parms);
- if (eval_ok == FALSE) {
- sprintf(resulttextp, "Bad parameter value: '%s'\n", parms);
- resulttextp = resulttext + strlen(resulttext);
- return;
- }
- outportb(ioadr, v & 0xFF);
- if (is16)
- outportb(ioadr, v >> 8);
- return;
- }
- v = inportb(ioadr);
- if (is16) {
- v += (inportb(ioadr) << 8);
- sprintf(resulttextp, "Channel %d read lobyte/hibyte: 0x%04X\n", chan, v);
- }
- else
- sprintf(resulttextp, "Channel %d read byte: 0x%02X\n", chan, v);
- resulttextp = resulttext + strlen(resulttext);
- return;
- }
-
- void do_command(char * cmd, char * parms) {
- unsigned int v;
- switch (cmd[0]) {
- case '?' :
- sprintf(resulttextp,
- "Command format: cmd [parms] [; cmd [parms]] [...]\n\n"
- "Commands on the same line are executed with interrupts locked out\n"
- "Values may be hex ('x' prefix), decimal ('d' prefix) or binary (default)\n"
- "\nCommands are:\n\n"
- "0 [value] - read [write] channel 0 data register\n"
- "1 [value] - read [write] channel 1 data register\n"
- "2 [value] - read [write] channel 2 data register\n"
- "00 [value] - read [write] channel 0 data register as lobyte/hibyte\n"
- "11 [value] - read [write] channel 1 data register as lobyte/hibyte\n"
- "22 [value] - read [write] channel 2 data register as lobyte/hibyte\n"
- "C value - write value to mode/command register\n"
- "R - read back timer 2 output via port B or C\n"
- "G [on|off] - read [set] timer 2 gate on port B\n"
- "S [on|off] - read [set] speaker gate on port B\n"
- "Q - quit\n"
- "\nExample command: g on; c 10110110; 22 x1234; s on\n"
- );
- resulttextp = resulttext + strlen(resulttext);
- break;
- case '0' :
- case '1' :
- case '2' :
- if (cmd[1] == cmd[0])
- rw_reg(TRUE, cmd[0] - '0', parms);
- else
- rw_reg(FALSE, cmd[0] - '0', parms);
- break;
- case 'c' :
- if (!parms[0]) {
- sprintf(resulttextp, "Must give parameter for 'c' command\n");
- resulttextp = resulttext + strlen(resulttext);
- return;
- }
- v = eval_value(parms);
- if (eval_ok == FALSE) {
- sprintf(resulttextp, "Bad parameter value: '%s'\n", parms);
- resulttextp = resulttext + strlen(resulttext);
- return;
- }
- outportb(0x43, v & 0xFF);
- break;
- case 'r' :
- sprintf(resulttextp, "Timer 2 readback on port B (AT) is %s; on port C (PC/XT) is %s\n",
- (inportb(0x61) & 0x20) ? "high" : "low",
- (inportb(0x62) & 0x20) ? "high" : "low");
- resulttextp = resulttext + strlen(resulttext);
- break;
- case 'g' :
- if (parms[0])
- outportb(0x61, (inportb(0x61) & 0xFE) | (parms[1] == 'n'));
- else {
- sprintf(resulttextp, "Timer 2 gate is currently %s\n",
- (inportb(0x61) & 0x01) ? "on" : "off");
- resulttextp = resulttext + strlen(resulttext);
- }
- break;
- case 's' :
- if (parms[0])
- outportb(0x61, (inportb(0x61) & 0xFD) | ((parms[1] == 'n') << 1));
- else {
- sprintf(resulttextp, "Speaker gate is currently %s\n",
- (inportb(0x61) & 0x02) ? "on" : "off");
- resulttextp = resulttext + strlen(resulttext);
- }
- break;
- case 'q' :
- asm sti;
- exit(0);
- default :
- if (parms[0])
- sprintf(resulttextp, "Bad command: '%s %s'\n", cmd, parms);
- else
- sprintf(resulttextp, "Bad command: '%s'\n", cmd);
- resulttextp = resulttext + strlen(resulttext);
- }
- return;
- }
-
- void do_commandline(char * s) {
- static char cmdbuf[LINELEN];
- static char parmbuf[LINELEN];
- unsigned int sp, dp1, dp2, endflags;
- char c;
- resulttextp = resulttext;
- asm cli;
- sp = 0;
- do {
- dp1 = 0; dp2 = 0;
- while ((s[sp] <= ' ') && (s[sp] != '\0'))
- ++sp; /* Skip leading whitespace */
- while ((s[sp] > ' ') && (s[sp] != ';')) {
- c = s[sp++];
- cmdbuf[dp1++] = tolower(c);
- }
- cmdbuf[dp1] = '\0';
- if ((s[sp] != '\0') && (s[sp] != ';')) {
- while ((s[sp] <= ' ') && (s[sp] != '\0'))
- ++sp; /* Skip whitespace */
- while ((s[sp] != '\0') && (s[sp] != ';')) {
- c = s[sp++];
- parmbuf[dp2++] = tolower(c);
- }
- }
- while (dp2) {
- if (parmbuf[dp2 - 1] <= ' ')
- --dp2;
- else
- break;
- }
- parmbuf[dp2] = '\0';
- if (dp1)
- do_command(cmdbuf, parmbuf);
- if (s[sp] == ';')
- ++sp;
- } while (s[sp]);
- asm pushf;
- asm pop endflags;
- asm sti;
- if (resulttextp != resulttext)
- write(1, resulttext, strlen(resulttext));
- if (endflags & 0x200)
- printf("\nWarning! Interrupts were inadvertently enabled during the command!\n");
- }
-
- void main(void) {
- static char inpbuf[LINELEN];
- unsigned int p;
- printf("Sample program #6 - Manipulates the CTC directly\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Type '?' for help, 'Q' to quit\n");
- while (1) {
- printf("\n>");
- if ((p = read(STDIN, inpbuf, LINELEN - 2)) > 0)
- --p;
- inpbuf[p] = '\0';
- do_commandline(inpbuf);
- }
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.24 HARDWARE PROBLEMS AND DIFFERENCES
-
- ## 7.24.1 DIFFERENCES BETWEEN THE INTEL 8253 AND 8254
-
- Though the 8254 was a "completely new design" from the 8253, the differences to
- the user or programmer are that the 8254 has the read-back command (see section
- »» 7.18), and the 8254 fixes a problem on the 8253 when used in mode 3 with a
- reload value of 3 (which does not concern us).
-
- ## 7.24.2 CHIPSET IMPLEMENTATIONS
-
- Differences in timer implementations in chipset ASICs are likely to be vague
- and unpredictable. Prof. John Mertus {JAM} (see section »» 1.7) has done some
- research on this, and found some machine-specific hardware differences. These
- are described in the applicable sections here, indicated with the marker {JAM}.
-
- One thing John discovered is that the PS/2 ASIC does not implement the read-back
- function (see section »» 7.18). Personally, I am p*ssed off at IBM for making
- such a cretinous and inconsiderate mistake. Because of them, we cannot just
- look at the machine type byte in the ROM and be sure that, if the machine is an
- AT-class machine, read-back will work - we must specifically test whether the
- machine supports read-back, and our programs may have to behave differently
- depending on the result of the test. Normally, clones are criticised for not
- being fully IBM compatible - this time, it is IBM! Rant mode off, dismount :-)
- BTW, {JAM} also reports that the 8254 CTC is implemented properly on the IBM
- ValuePoints. Any information on other machines would be welcomed. (*)
-
- ## 7.24.3 INTEL 8253/8254/82C54 CLOCK SYNCHRONISATION PROBLEMS
-
- This information is from Intel Q&A and application notes, and was sent to me by
- Louis Warshaw (louis@gate.net). Thanks Louis!
-
- Unfortunately I found the Intel documentation very vague, so I will quote the
- relevant parts and hope that Intel don't sue me :-) The problems concern
- synchronisation between the CTC clock input (the 1.193182 MHz clock) and the
- write access pulses when the data registers are written or when a counter latch
- command is issued.
-
- -WR ────────────────┐ ┌───────────────────────────
- └───────┘
- ^a
- CTC Clk ┌───────────────────
- ────────────────────────────────┘
- ^b
-
- The timing diagram shows a write access to a data register (I/O address 40h,
- 41h, or 42h) and a rising edge on the CTC clock. The chip's specification for
- the time between point 'a' and point 'b' is called Twc, and is specified as 55
- nanoseconds maximum for the Intel 8254.
-
- Here is what the Intel documentation says. My comments are in square brackets.
-
- "Question: Why is Twc specified to the rising edge of [CTC] Clock, but
- yet Clocks are loaded [sic] on the falling edge?
- "Answer: This is used for software synchronisation of loading a new
- count [reload value]. The new value must be in the Twc window
- to guarantee that the new count [reload value] is loaded on the
- next falling edge [of CTC clock]."
-
- I think this is just saying that the reload register must be fully loaded before
- the rising edge of CTC Clock, in order to be decremented on the following
- falling edge of CTC Clock. I assume that if the reload register is not loaded
- at least Twc nanoseconds before the rising edge, the chip will just wait for the
- next rising edge, thus there is an uncertainty of one CTC Clock width as to
- exactly _which_ CTC Clock will start decrementing the counting register, and
- this depends on the reload register becoming fully loaded at least shortly
- before the rising edge before the falling edge that will decrement it.
-
- "Question: Why should Gate be pulsed immediately following a write of a
- new count [reload] value, when using an asynchronous clock
- source [CTC Clock not synchronous with the Write pulse] in
- modes two and three?
- "Answer: If an asynchronous clock input is used for a counter [channel],
- you need to use Gate to synchronise the loading of the new count
- [reload value]."
-
- As for the second point, Intel's question and answer are so vague that I can
- not come to any conclusion about the implications for the programmer.
-
- "Question: What does the comment on page 3-74, figure 17, Note,
- Peripheral Components, 1993 mean? "NOTE: A Gate transition
- should not occur one [CTC] clock prior to terminal count".
- "Answer: Modes 2 and 3 use the [CTC] clock frequency for the Rate
- Generator and Square Wave Mode respectively. In modes 2 and
- 3, the 8254 (and 82C54) uses "look ahead" logic to precondition
- OUT to go low on the falling edge of the CLK input upon
- terminal count. Without this look ahead feature, the 8254
- would not have time to resolve its internal logic at the same
- time OUT is to go low upon reaching terminal count. Monitoring
- the count value in software, before disabling counting via the
- Gate, is usually sufficient to prevent this combination of
- events. This has always been the operation of the 8254 (and
- 8253, and 82C54) and no problems resulting from this [sic]."
-
- Again nice and vague. I think this is saying that terminal count is anticipated
- by the look-ahead logic one CTC clock before it actually occurs, i.e. in mode 2
- when the Counting Register reaches two, and if Gate goes low while the Counting
- Register is two, the output may actually go low as normal on the next CTC clock
- even though the Gate input is low. I wonder how this relates to mode 3.
-
- Two more problems are described. These apply only to the 82C54, the CMOS
- version of the 8254. I do not know whether any PCs actually use the 82C54.
-
- There are two 'failure modes' documented - the Twc count write failure mode and
- the Tcl counter latch command failure mode.
-
- "The Twc [counter write] failure mode occurs in a very narrow window
- between the Twc min and Twc max timing when writing the last [or only]
- byte of a count [Reload register] value. The Twc specification defines
- the relationship between the writing of a count [Reload] value and the
- Clk [CTC clock] pulse and whether the Clk pulse will or will not be
- reflected in the subsequent counting operation. The Clk pulse is a
- low to high transition on one of the 82C54's Clk input pins.
- [The 82C54 documentation states Twc min = 0, Twc max = 55 ns].
-
- -WR ────────────────┐ ┌───────────────────────────
- └───────┘
- │<─Twc─>│
- CTC Clk ┌───────────────────
- ────────────────────────────────┘
-
- "If the rising edge of a Clk pulse happens before the Twc min
- specification then it is too early and will not be reflected in
- the count. If the Clk pulse happens after the Twc max specification
- then the Clk pulse will be reflected in the count. If the Clk happens
- between Twc min and Twc max it may or may not be reflected in the count
- value.
- Twc min is 0 ns and Twc max is 55 ns or a 55 ns window [sic].
-
- "There is a worst case 8-20 ns [floating] window between Twc min and
- Twc max where the 82C54 counter control logic is corrupted and the
- counter enters an undefined state. The counter must be re-initialised
- by rewriting the counter Mode word. The problem is worse at cold
- temperatures (0 degrees C) and low VCC (4.5V).
- Only the counter being written to is affected.
- The other counters continue to count properly.
-
- "The Twc failure mode actually varies across the normal skew of the
- fabrication process. The 82C54's typical wafer fabrication process
- failure mode window is between 300 picoseconds to 1 nanosecond. The
- actual window may typically be less but this represents the +/- 100
- picosecond resolution of the Teradyne test computer used to characterise
- the Twc failure mode. When the process shifts within the normal skew to
- the slow implant corner the failure mode window increased [sic] to a
- worst case of 8-20 nanoseconds.
-
- "The failure mode is a function of an asynchronous Clk and -WR input
- signals. When -WR and Clk are asynchronous the -WR may occur at any
- time in relation to the Clk. If -WR and Clk are synchronous -WR will
- always occur in the same relation ship [sic :-)] to Clk. The 82C54
- Clk and -WR inputs are synchronous when the Clk input is the system
- microprocessor clock, or a derivative of it. If the 82C54 Clk source
- is independent of the system clock then the -WR and Clk are
- asynchronous unless hardware synchronised external [sic] to the 82C54.
-
- "There are three modifications which compensate for the failure mode:
-
- "1. Use a Clk input signal which is a derivative of the system
- microprocessor clock source. This makes the interaction of the
- -WR and Clk totally predictable. The -WR and Clk will not happen
- coincidentally and the synchronisation prohibits occurrence of a
- -WR within the failure mode window time of Clk.
-
- "2. Through the use of the 82C54 Read Back Command the software
- detects the state of the Counter Status byte Null Count flag which
- indicates whether the count has been moved from the Count Register
- [Reload register] to the Counting Element (CE) [Counting register]
- or "loaded". See Figure 1 Internal Block Diagram of a Counter (
- Figure 5, 82C54 Data Sheet). Unless the Null Count flag is cleared
- the count has not been successfully loaded. If the Null Count flag
- is not cleared then the software rewrites the Mode word and count
- value [Reload value].
-
- "3. Externally synchronise the -WR and Clk input signals. This is done
- by gating -WR with Clk. The -WR and Clk inputs then appear
- synchronous to the 82C54 which prohibits the occurrence of a -WR
- within the failure mode window time of Clk."
-
- As far as I can tell from discussion with Louis Warshaw, the problem affects
- writing a reload value on-the-fly to a CTC channel. The -WR signal on the
- timing diagram represents the pulse issued by the processor to write the last
- or only byte of the new reload value to the channel. The problem occurs if
- the rising edge of the Clk to that channel occurs within a certain time of the
- trailing (rising) edge of the write. There is a timing window which is between
- 8 and 20 ns wide, and may dynamically shift within the 0 to 55 ns specification
- Twc window, relative to the rising edge of -WR. If a rising edge of Clk appears
- within this 8 to 20 ns wide window, the internal logic of the counter will be
- corrupted and the counter will go into never-never land until reinitialised by
- a Mode write to the Mode/Command register.
-
- "Counter Latch Command failure mode, Tcl
-
- "The failure mode occurs during a very narrow window between -WR and
- Clk when latching a count [Counting register] value. The approximately
- 10 nanosecond window between -WR rising edge and -Clk falling edge, when
- asynchronously writing a Counter Latch or Read Back command, the count
- value read may be in error. The byte value read is not in sequence in
- relation to the previous or following byte read. The Counting Element
- [Counting register] and counter control logic are unaffected by the
- failure mode and continues [sic] to decrement properly.
-
- -WR ────────────────────────────┐ ┌───────────────
- └───────┘
- │<───Tcl───>│
- CTC Clk ────────────────────────┐
- └───────────────────────────
-
- "The error window has been verified on a Teradyne test computer to be a
- 200-300 picoseconds [sic] window between -WR rising edge and Clk falling
- edge when writing a Counter Latch or Read Back command.
-
- "The failure mode is not a violation of the Tcl specification. The Tcl
- specification tells the user a Clk pulse falling edge which happens
- close to the -WR rising edge of a Counter Latch or Read Back command
- will (Tcl min) or will not (Tcl max) be reflected in the count value
- subsequently read from the Counter Output Latch [Latch register]. The
- Tcl specification provides for a +/- one Clk pulse, or one bit error,
- in the count value latched. The failure mode results in a multiple bit
- error in the count value read [from the Latch register].
-
- "There are three modifications which compensate for the failure mode:
-
- "1. Use a Clk signal which is a derivative of the system microprocessor
- clock source. This makes the interaction of the -WR and Clk [sic]
- totally predictable. The -WR and Clk never happen coincidentally
- and the synchronisation prohibits occurrence of a WRX [sic] within
- the failure mode window time of Clk.
-
- "2. Latch and read the count twice if an error greater than one bit
- error occurs.
-
- "3. Externally synchronise the -WR and Clk input signals. This is done
- by gating -WR with Clk. -WR and Clk then appear synchronous to the
- 82C54 which prohibits the occurrence of a -WR within the failure
- mode window time of Clk.
-
- This is saying that if the -WR access on a counter latch or read-back command
- falls within a narrow window a certain length of time after the falling edge of
- the Clk to that channel, an incorrect count value is latched in the Latch
- register. Presumably this occurs because the value provided by the Counting
- register becomes briefly invalid a short time after the Counting register
- decrements, and if the latch command happens to occur during that short time,
- the invalid value will be latched into the Latch register. Thus, with an 82C54
- where the Clk is not synchronous with -WR, you cannot trust the value latched
- by a Counter Latch or read-back command.
-
- ## 7.25 IS THE CTC AN 8253 OR AN 8254?
-
- Well, you can check the BIOS Machine Type Byte at location F000:FFFE. Values
- 0xFD, 0xFE, and 0xFF indicate PCjr, XT/Portable, and original PC respectively,
- all of which have 8253 CTCs. A Type Byte value of 0xFC indicates an AT or
- later machine, which should have an 8254. In other words,
-
- -------------------------------- snip snip snip --------------------------------
- unsigned int is_machine_an_AT(void) {
- return (*(unsigned char far *)MK_FP(0xF000, 0xFFFE) == 0xFC);
- }
- -------------------------------- snip snip snip --------------------------------
-
- However, this method is not foolproof, because some clones may not have a valid
- Machine Type byte, and because of IBM's brilliance in not implementing a proper
- 8254 in the PS/2's ASIC. With this test, some clones, and PS/2 machines, would
- report an 8254 when they may only have an 8253. Also, when the CTC is emulated,
- as it is for DOS applications running under OS/2, and presumably under Linux,
- the full functionality of the CTC may not be available.
-
- The sample program in section »» 7.26 contains some code that determines
- whether the CTC is an 8253 or an 8254 or something else (i.e. a faulty chip or
- a partially emulated chip) which can be extracted and used to determine the CTC
- type, but note that it leaves CTC channel two in a non-standard mode (mode 0).
- This should not be a problem, as channel two is used for audio generation and
- is fully initialised by any code that will subsequently use that channel.
-
- ## 7.26 DETERMINING THE EXACT STATE OF THE CTC
-
- You might need to determine the state of a particular channel in the timer chip.
- The only things you can really find out are the state of the lobyte/hibyte flag
- (this cannot be read directly, but its state can be inferred by reading the
- count several times, assuming the channel is being clocked), and the programmed
- operating mode and BCD/binary flag, which can be determined by the read-back
- command (assuming that the timer chip is an 8254).
-
- The logic of the function infer_lobyte_hibyte_flag() in the program in section
- »» 7.27 is: read the count, then repeatedly re-read the count until a different
- value is obtained. Then infer the state from whether the lobyte or the hibyte
- of the value has changed. If the lobyte changed, then the flag is in sync
- (normal). If the hibyte changed, then the flag is out of sync. In the latter
- case, the flag can be brought into sync by reading the data register once.
-
- For each counter read operation, the count is latched prior to being read.
- The routine disables interrupts, to ensure that the number of CTC clocks
- between reads is minimal.
-
- ## 7.27 SAMPLE PROGRAM: REPORT CHANNEL STATES
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #7
- Reports CTC type (8253, 8254, or faulty/emulated) and the operating states
- (lobyte/hibyte flag, mode, binary/BCD, output state) of all channels.
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE7.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample7.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <bios.h>
- #include <dos.h>
- #include <process.h>
- #include <stdio.h>
- #include <stdlib.h>
-
- #define TESTVALUE 0x55AA /* Value to use as reload test value */
-
- #define BACKWARDS ((unsigned int)((TESTVALUE >> 8) + ((TESTVALUE & 0xFF) << 8)))
- /* Backwards TESTVALUE */
-
- #define EXP_CSTAT 0x30 /* Expected counter status */
-
- #define CTC_EMUL 0 /* CTC is faulty or emulated by OS */
- #define CTC_8253 1 /* CTC is an 8253 */
- #define CTC_8254 2 /* CTC is an 8254 */
-
- #define LHF_INSYNC 0 /* Lobyte/hibyte flag is in sync */
- #define LHF_OUTSYNC 1 /* Lobyte/hibyte flag is out of sync */
- #define LHF_UNKNOWN 2 /* Lobyte/hibyte flag cannot be determined */
-
- typedef struct {
- unsigned int count;
- unsigned char status;
- } readback_data;
-
- /* Code */
-
- unsigned int read_channel_raw(unsigned int channum) {
- unsigned int cv;
- if (channum < 3) {
- asm pushf;
- asm cli;
- outportb(0x43, channum << 6);
- cv = inportb(0x40 + channum);
- cv += inportb(0x40 + channum) << 8;
- asm popf;
- }
- return cv;
- }
-
- /* Simple short delay - just wait for at least one CTC clock to occur */
-
- void wait_ctc_clock(void) {
- unsigned int ch0count;
- ch0count = read_channel_raw(0);
- while (read_channel_raw(0) == ch0count)
- ;
- return;
- }
-
- /* The following function is described in section »» 7.18 */
-
- void readback_channel(unsigned int channum, readback_data * rbdp) {
- if (channum < 3) {
- asm pushf; /* Preserve interrupt flag */
- asm cli; /* Disable interrupts */
- outportb(0x43, 0xC0 + (2 << channum)); /* Latch count, status */
- rbdp->status = inportb(0x40 + channum); /* Get status */
- rbdp->count = inportb(0x40 + channum); /* Get count lobyte */
- rbdp->count += inportb(0x40 + channum) << 8; /* Get count hibyte */
- asm popf; /* Restore interrupt flag */
- return;
- }
- }
-
- /* This function determines the CTC type. It stores the current contents of
- Port B (speaker and timer 2 gate control port), then turns off speaker
- enable and sets timer 2 gate low. It then attempts to read-back the
- status of CTC channel 2, and stores this in ch2rbd.status; this will be
- used to restore channel 2 to its original operating state if it turns out
- that the CTC is an 8254.
- The function then programs CTC channel 2 for mode zero with a reload value
- specified by TESTVALUE. In this mode, channel 2 will reload on the next
- CTC clock, and will not decrement, as its gate input is low. We then wait
- for at least one CTC clock to occur (detect this by reading CTC channel 0
- and waiting for a change in the latched value). CTC channel 2 then contains
- a known value, and is in a stable state. We know the expected latched count
- value and the expected status value to be returned on a read-back. It is
- then possible to determine the CTC type by reading a few things and looking
- at the CTC's responses. Specifically, the routine checks that it can latch
- and read a stable value equal to the reload register value - if this fails,
- the CTC is assumed to be faulty or emulated. Then it issues a read-back
- command and keeps the read-back status byte, then latches and reads the
- count. If the CTC is an 8253, this will yield a 'backwards' count - i.e.
- TESTVALUE with hibyte and lobyte interchanged, because the lobyte/hibyte
- flag was reversed by the read-back (which reads the data register three
- times). On an 8254, this latch and read will yield TESTVALUE.
- Next, it performs another readback (to reinstate the original lobyte/hibyte
- flag if the CTC is an 8253) and keeps the read-back status again, then it
- latches and reads the count again. This should _always_ yield TESTVALUE,
- for either an 8253 or 8254.
- It then checks for the expected behaviour of an 8253 and an 8254 separately.
- If the CTC does not give the correct response, it will be reported as faulty
- or emulated.
- Note that this function spends most of its time with interrupts locked out.
- */
-
- unsigned int detect_ctc_type(void) {
- unsigned int ctctype; /* Value to be returned */
- unsigned int port61; /* Port 61h value */
- readback_data ch2rbd, rbd1, rbd2; /* Read-back storage */
- unsigned int backwards, forwards; /* Latched count values */
-
- ctctype = CTC_EMUL; /* Assume faulty or unknown CTC type */
-
- asm pushf;
- asm cli;
-
- port61 = inportb(0x61); /* Get Port B value */
- outportb(0x61, port61 & 0xFC); /* Turn off timer 2 gate and speaker */
-
- /* Try read-back on channel two, only useful if CTC turns out to be an 8254 */
-
- readback_channel(2, &ch2rbd);
- readback_channel(2, &ch2rbd); /* Attempt to read-back channel two */
-
- outportb(0x43, 0xB0); /* Channel 2, two bytes, mode 0, binary */
- outportb(0x42, TESTVALUE & 0xFF); /* Lobyte of reload value */
- outportb(0x42, TESTVALUE >> 8); /* Hibyte of reload value */
-
- wait_ctc_clock();
- wait_ctc_clock(); /* Wait for a couple of CTC clock pulses */
-
- /* Just read the raw value a couple of times, make sure it's stable */
-
- if ((read_channel_raw(2) != TESTVALUE) ||
- (read_channel_raw(2) != TESTVALUE))
- goto got_type; /* Structured programming? Never heard of it */
-
- /* Try a read-back - on an 8253, this will reverse the lobyte/hibyte flag */
-
- readback_channel(2, &rbd1);
-
- /* Read the count - on an 8253 this will be TESTVALUE backwards, on an 8254
- it will be TESTVALUE */
-
- backwards = read_channel_raw(2);
-
- /* Try another read-back, into rbd2 this time */
-
- readback_channel(2, &rbd2);
-
- /* Now latch and read the count again */
-
- forwards = read_channel_raw(2);
-
- /* Now, try to figure out what it is! */
-
- if ((rbd1.status != EXP_CSTAT) && (rbd2.status != EXP_CSTAT) &&
- (backwards == BACKWARDS) && (forwards == TESTVALUE))
- ctctype = CTC_8253;
- if ((rbd1.status == EXP_CSTAT) && (rbd2.status == EXP_CSTAT) &&
- (backwards == TESTVALUE) && (forwards == TESTVALUE))
- ctctype = CTC_8254;
- got_type:
-
- /* Now we know what it is. If it's an 8254, we can restore channel 2 to its
- previous mode, although we cannot restore the original divisor, because
- we can't tell what it was. If it's not an 8254, we can't fix anything */
-
- if (ctctype == CTC_8254) {
- outportb(0x43, 0x80 + (ch2rbd.status & 0x3F));
- outportb(0x42, 0);
- outportb(0x42, 0);
- }
-
- outportb(0x61, port61); /* Restore speaker and timer 2 control bits */
-
- asm popf;
- return ctctype;
- }
-
- unsigned int test_delta(unsigned int latchv, unsigned int cport) {
- unsigned int nreads, startcount, count, diff, hbdiff, lbdiff;
- asm pushf;
- asm cli;
- outportb(0x43, latchv); /* Read count to startcnt */
- startcount = inportb(cport);
- startcount += inportb(cport) << 8;
- for (nreads = 0; nreads < 20; ++nreads) {
- outportb(0x43, latchv); /* Latch count again */
- count = inportb(cport);
- count += inportb(cport) << 8;
- diff = startcount ^ count; /* Get difference */
- hbdiff = ((diff & 0xFF00) != 0);
- lbdiff = ((diff & 0x00FF) != 0);
- if (lbdiff == hbdiff) /* Both or neither changed */
- continue; /* Wait for difference */
- if (lbdiff) { /* Lobyte changed */
- asm popf;
- return LHF_INSYNC; /* Flag is in sync */
- }
- if (hbdiff) { /* Hibyte changed */
- asm popf;
- return LHF_OUTSYNC; /* Flag is out of sync */
- }
- } /* for nreads */
- asm popf;
- return LHF_UNKNOWN; /* Couldn't determine */
- }
-
- /* The following function infer_lobyte_hibyte_flag() attempts to determine the
- state of the lobyte/hibyte flag for a specified CTC channel, assuming that
- that channel has been programmed for lobyte/hibyte access (i.e. bits 5 and 4
- of the control register were 1,1 at initialisation). It should work for
- both the 8253 and 8254. The lobyte/hibyte flag toggles every time the
- latched (or unlatched) count is read from the counter data register. The
- function returns LHF_INSYNC, LHF_OUTSYNC, or LHF_UNKNOWN. This function
- seems to be reliable on fast machines but does not seem to work well on
- slow machines or XTs (I don't know why), so don't rely on its accuracy! */
-
- unsigned int infer_lobyte_hibyte_flag(int channum) {
- unsigned int latchv, cport, result;
- unsigned int progress[3];
-
- if (channum > 2)
- return LHF_UNKNOWN;
-
- latchv = channum << 6;
- cport = 0x40 + channum;
-
- progress[LHF_INSYNC] = 0;
- progress[LHF_OUTSYNC] = 0;
- progress[LHF_UNKNOWN] = 0;
-
- do
- result = test_delta(latchv, cport);
- while (++progress[result] < 10);
- return result;
- }
-
- void main(void) {
- unsigned char machtype; /* Machine type byte */
- readback_data rbd[3]; /* Readback data structures */
- unsigned int lhf[3]; /* Lobyte/hibyte flag values */
- unsigned int ch; /* Channel number */
- unsigned int port61; /* Port 61h value */
-
- static char machname[4][31] = {
- "an AT class machine (8254 CTC)", /* 0xFC */
- "a PCjr (8253 CTC)", /* 0xFD */
- "a PC/XT (8253 CTC)",
- "an IBM-PC (8253 CTC)"
- };
-
- printf("Sample program #7 - Reports CTC type, modes, and output states\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
-
- machtype = *(unsigned char far *)MK_FP(0xF000, 0xFFFE);
- if (machtype < 0xFC)
- printf("The BIOS Machine Type byte has a non-standard value\n\n");
- else
- printf("The BIOS Machine Type byte says this machine is %s\n\n", machname[machtype - 0xFC]);
-
- switch (detect_ctc_type()) {
- case CTC_EMUL:
- printf("CTC appears to be faulty, non-standard, or emulated by operating system\n\n");
- printf("Cannot determine operating parameters\n");
- break;
- case CTC_8253:
- printf("CTC is an 8253\n\nCannot determine operating modes; attempting to determine\n");
- printf("lobyte/hibyte flag state assuming lobyte/hibyte access and mode 2 or 3\n\n");
- for (ch = 0; ch < 3; ++ch) {
- switch (infer_lobyte_hibyte_flag(ch)) {
- case LHF_INSYNC:
- printf("Channel %d lobyte/hibyte flag sync:\tCorrect\n", ch);
- break;
- case LHF_OUTSYNC:
- printf("Channel %d lobyte/hibyte flag sync:\tReversed\n", ch);
- break;
- default:
- printf("Channel %d lobyte/hibyte flag sync:\tCannot be determined\n", ch);
- }
- } /* for ch */
- break;
- case CTC_8254:
- printf("CTC is an 8254; all information is available\n\n");
- port61 = inportb(0x61);
- outportb(0x61, (port61 & 0xFC) | 0x01); /* Enable timer 2 gate */
- for (ch = 0; ch < 3; ++ch) {
- readback_channel(ch, &rbd[ch]);
- lhf[ch] = infer_lobyte_hibyte_flag(ch);
- }
- printf("Parameter\t\tChannel 0\tChannel 1\tChannel 2\n\n");
- printf("Access sequence:");
- for (ch = 0; ch < 3; ++ch) {
- switch (rbd[ch].status & 0x30) {
- case 0x00:
- printf("\tUninitialised");
- rbd[ch].count = 0;
- break;
- case 0x10:
- printf("\tLobyte only");
- rbd[ch].count &= 0xFF;
- break;
- case 0x20:
- printf("\tHibyte only");
- rbd[ch].count &= 0xFF00;
- break;
- case 0x30:
- printf("\tLobyte/hibyte");
- } /* switch */
- } /* for ch */
- printf("\n");
- printf("Operating mode:\t\t%d\t\t%d\t\t%d\n",
- (rbd[0].status >> 1) & 0x07,
- (rbd[1].status >> 1) & 0x07,
- (rbd[2].status >> 1) & 0x07);
- printf("BCD/binary mode:");
- for (ch = 0; ch < 3; ++ch)
- printf(rbd[ch].status & 1 ? "\tBCD\t" : "\tBinary\t");
- printf("\n");
- printf("Output pin state:");
- for (ch = 0; ch < 3; ++ch)
- printf(rbd[ch].status & 0x80 ? "\tHigh\t" : "\tLow\t");
- printf("\n");
- printf("Null Count flag:");
- for (ch = 0; ch < 3; ++ch)
- printf(rbd[ch].status & 0x40 ? "\tSet\t" : "\tClear\t");
- printf("\n");
- printf("Current raw count:\t0x%04X\t\t0x%04X\t\t0x%04X\n",
- rbd[0].count, rbd[1].count, rbd[2].count);
- printf("Lobyte/hibyte flag:");
- for (ch = 0; ch < 3; ++ch) {
- if ((rbd[ch].status & 0x30) == 0x30) {
- switch (lhf[ch]) {
- case LHF_INSYNC:
- printf("\tCorrect\t");
- break;
- case LHF_OUTSYNC:
- printf("\tReversed");
- break;
- default:
- printf("\tUnknown\t");
- }
- }
- else
- printf("\tN/A\t");
- } /* for */
- printf("\n");
- asm cli;
- outportb(0x61, (inportb(0x61) & 0xFC) | (port61 & 0x03));
- /* Restore timer 2 gate and speaker control bits */
- asm sti;
- break;
- } /* switch ctctype */
-
- exit(0);
- }
-
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.28 CTC ACCESS UNDER OS/2
-
- Native OS/2 applications do not need to access the CTC directly. This section
- is concerned with DOS applications that can be run under OS/2 in a VDM (Virtual
- DOS Machine).
-
- The HW_TIMER option for the DOS session determines whether the DOS application
- is given access to the real CTC, or whether it uses the virtual CTC driver,
- VTIMER.SYS. Manipulating the CTC with the HW_TIMER option set ON may cause
- interference with other DOS tasks, though I think it does not affect OS/2
- because OS/2 uses the Real Time Clock for its timekeeping. I believe that OS/2
- does not use CTC channel zero itself; it is only required for DOS tasks.
-
- OS/2's VTIMER.SYS (virtual CTC emulator, used if HW_TIMER is OFF) is rather
- interesting. I have paraphrased some information from the OS/2 red book (OS/2
- Version 2.0 Volume 2: DOS and Windows Environment), if anyone has more detailed
- or newer information I'd like to see it. (*)
-
- ## 7.28.1 OS/2 VTIMER.SYS: CTC CHANNEL ZERO
-
- VTIMER.SYS is able to generate virtual (emulated) interrupts at 54.9254 ms
- intervals, or 13.7314 ms intervals (four times faster). If the DOS session
- reprograms the divisor of channel zero, it gets its ticks at 13.7314 ms
- intervals, regardless of the actual divisor value it programmed (presumably
- unless it programmed a divisor of 65536). VTIMER.SYS runs the real CTC channel
- zero at 54.9254 ms, or 13.7314 ms if one or more DOS sessions are running at
- this rate. The 13.7314 ms interrupt capability is required for GW-BASIC which
- uses a four times faster interrupt for its PLAY command (music).
-
- Latching channel zero causes a "random value derived from the system time" to be
- loaded into the emulated Latch register, which can then be read. This design
- decision was based on the fact that the count register is often used to provide
- a random number seed, and this approach supposedly gives the DOS application a
- "sense of elapsed time". The documentation does not say whether read-back is
- supported, but I suspect it is not.
-
- In other words, it is not possible to use channel zero for timing in a DOS
- session under OS/2, unless HW_TIMER is set to ON. Stick to the tick count
- variable and/or timer interrupts, at the normal rate, if you want your program
- to run properly under OS/2.
-
- ## 7.28.2 OS/2 VTIMER.SYS: CTC CHANNEL ONE
-
- Apparently this channel only supports read accesses, which presumably return a
- random number or a number derived from the system time. All other accesses are
- ignored. At a guess, I would say that each read of the data register will yield
- a random or time-derived value.
-
- ## 7.28.3 OS/2 VTIMER.SYS: CTC CHANNEL TWO
-
- This is interesting. Channel two is linked up with the emulated Port B (see
- section »» 7.5) and OS/2 "serialises" speaker access from different tasks.
- When we generate a speaker tone, we program the divisor into channel two (which
- will determine the tone frequency), and then enable the speaker by means of the
- bottom two bits in Port B. VTIMER.SYS remembers the divisor value, and when
- the bits are set in Port B, it calls the OS/2 "kernel beep", which may block if
- it is already beeping on behalf of a different process. After completion of any
- beep in progress, the kernel beep function programs the correct value into the
- real channel two, and programs the real Port B to start the beep. When the DOS
- session turns off the bits in Port B, the beep is stopped and the kernel beep
- becomes available for use by other sessions.
-
- The "serialisation" can be pre-empted by an "interrupt time beep service" which
- is somehow used if the beep is issued by the keyboard scancode interrupt
- handler, to support the "keyboard buffer full" beep issued by the BIOS in a
- DOS session. Interesting!
-
- ## 7.29 GENERATING AUDIO TONES ON THE SPEAKER
-
- Although this is unrelated to timing...
-
- The PC speaker interface circuitry is thoroughly documented in section »» 7.5.
-
- CTC channel two is used for generating audio. It is normally operated in mode
- three, to produce a square wave signal. It can be used in different ways,
- though - see section »» 10.7.1 for the PWM audio generation technique.
- The speaker interface is controlled by two bits in the read/write register
- at I/O address 61 hex:
-
- 7 6 5 4 3 2 1 0
- * * * * * * . . Not applicable to speaker control - do not modify!
- . . . . . . * . Speaker data
- . . . . . . . * Timer 2 Gate
-
- The Timer 2 Gate signal is directly connected to the 'gate' input of timer
- channel two. This signal must be high in order for the counter to decrement.
- When the gate signal goes low, the timer output goes high immediately, and
- counting ceases. The count register is reloaded from the divisor register on
- the next 1.193182 MHz clock pulse, and when the gate input goes high again,
- counting resumes starting at the divisor value, thus synchronising the counter.
-
- The Speaker data output is logical-ANDed with the output from timer two, to
- drive the speaker. Thus, to generate a tone using timer 2, Speaker data should
- be set to '1' and Timer 2 gate should also be set to '1'. The frequency of the
- tone will be 1193181.6666... divided by the divisor value programmed into CTC
- channel two.
-
- To generate audio by bit manipulation, Timer 2 gate should be set to zero.
- This disables timer two and forces its output high. The speaker can then be
- directly controlled via Speaker data. Setting this bit high allows current
- to flow in the speaker coil, causing the cone to move outwards (or inwards,
- depending on which way the speaker is wired - it doesn't matter really).
- Setting the bit low causes the cone to return to its normal position.
- Toggling the bit at rate of n toggles per second gives a frequency of n/2 Hz.
-
- ## 7.30 SAMPLE PROGRAM: GENERATING A TONE USING CTC CHANNEL TWO
-
- The following program generates a tone at approximately 1KHz for approximately
- one second, using CTC channel two.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #8
- Demonstrates generating a tone using timer channel two
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE8.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample8.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
-
- #define DIVISOR(frequency) ((unsigned int) ((1193181.6666 / frequency) + 0.5))
-
- #define FREQUENCY 1000 /* Tone frequency in Hz */
-
- unsigned long read_bios_tick_count(void) {
- unsigned long ct;
- asm pushf;
- asm cli;
- ct = * BIOS_TICK_COUNT_P;
- asm popf;
- return ct;
- }
-
- int has_tick_occurred(void) {
- static unsigned long old_tick_count = 0xFFFFFFFFL;
- if (read_bios_tick_count() != old_tick_count) {
- old_tick_count = read_bios_tick_count();
- return TRUE;
- }
- return FALSE;
- }
-
- void init_channel(unsigned int channum, unsigned int accessmode,
- unsigned int mode, unsigned int reload) {
- if (channum > 2 || accessmode < 1 || accessmode > 3)
- return;
- asm pushf;
- asm cli;
- outportb(0x43, (channum << 6) + (accessmode << 4) + ((mode & 0x07) << 1)); /* Mode */
- outportb(0x40 + channum, reload & 0xFF); /* Reload reg lobyte */
- outportb(0x40 + channum, (reload >> 8) & 0xFF); /* Reload reg hibyte */
- asm popf;
- return;
- }
-
- void turn_tone_on(unsigned int divisor) {
- init_channel(2, 3, 3, divisor); /* Channel 2, 16-bit, mode 3 */
- asm pushf; /* Preserve interrupt flag */
- asm cli;
- outportb(0x61, inportb(0x61) | 0x03); /* Enable timer and speaker */
- asm popf; /* Restore interrupt flag */
- return;
- }
-
- void turn_tone_off(void) {
- asm pushf; /* Preserve interrupt flag */
- asm cli;
- outportb(0x61, inportb(0x61) & 0xFC); /* Disable speaker */
- asm popf; /* Restore interrupt flag */
- return;
- }
-
- void main(void) {
- unsigned int n = 0;
-
- printf("Sample program #8 - Demonstrates generating a tone using CTC channel two\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Tone frequency is %d Hz\n", FREQUENCY);
-
- has_tick_occurred(); /* Init has_tick_occurred() */
- while (has_tick_occurred() == FALSE)
- ; /* Wait for a tick to occur */
- turn_tone_on(DIVISOR(FREQUENCY));
- while (n < 18) /* Stop after one second */
- if (has_tick_occurred())
- ++n;
- turn_tone_off();
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.31 TIMING SHORT PERIODS USING CTC CHANNEL TWO
-
- {JAM} The ideas and code example in this section are largely from Prof.
- John Mertus's document.
-
- Reading a CTC channel requires three I/O accesses, and I/O accesses are
- notoriously slow by comparison to memory accesses, particularly on fast
- machines. Referring to section »» 7.5, CTC channel two's output is readable
- on bit 5 of Port C at I/O address 62h (PC and XT) or bit 5 of Port B at I/O
- address 61h (AT and later), and this port can be read in a single I/O access.
- This can be useful when a short (microsecond-level) delay must be timed
- especially accurately.
-
- With this approach, CTC channel two is used in mode zero (see section »» 7.8.2),
- the 'interrupt on terminal count' mode. In this mode, we can write a count
- value to the CTC, then watch the Timer 2 Output signal and wait for it to go
- high, signalling that the time period has expired.
-
- When using this technique, you must ensure that the Timer 2 Gate output on
- bit 0 of Port B at I/O address 61h is high (CTC channel two will not count
- if this signal is low) and that the Speaker Data signal on bit 1 of the same
- register is low, to avoid sending horrible noises to the speaker!
-
- The following code fragment shows how to produce a short (five microseconds
- plus overhead) pulse on the strobe output of a parallel port, using this
- approach. It will not work on the old PC and XT - see the comment by the
- WaitCTC: label.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- You could also time longer periods, up to 54.9 ms, but in this case you would
- remove the PUSHF/CLI and STI around the delay code and set and clear the bit
- in the parallel port register in a different way - to clear it, use PUSHF/CLI,
- read the port, AND the value, write it back, and POPF, and same to set the bit
- (but use OR instead of AND, of course). This leaves the body of the delay
- loop operating with interrupts enabled, which is desirable to avoid problems
- with interrupt latency, etc, but could cause problems for example if the
- keyboard buffer filled up and a beep was issued, because this would result
- in Port B and CTC channel 2 being reprogrammed part-way through the delay loop.
- A pop-up TSR could also issue a beep, causing the same problem. This would
- require intercepting int 10h (BIOS video output, generates a beep) and int 9
- (keystroke interrupt) or int 15h keystroke intercept, and even then, this would
- not prevent some interrupt-triggered code from reprogramming CTC channel 2.
- In other words, I can't see any safe way to implement longer delays with
- interrupts enabled using this technique (except in a controlled environment).
-
- -------------------------------- snip snip snip --------------------------------
- NCTCClocks EQU 6 ; Six CTC clocks (5us) for the delay
- LPTPortBase DW 3BCh ; Set to your LPT port base address
-
- ; Somewhere in the initialisation code, set up Timer 2 Gate and Speaker Data:
-
- pushf
- cli ; No interrupts
- in al,61h ; Get Port B
- and al,11111101b ; Turn off Speaker Data
- or al,00000001b ; Turn on Timer 2 Gate
- out 61h,al ; Write it back
- popf ; Restore interrupt flag
-
- ; ...
-
- ; Then produce the short pulse:
-
- mov dx,LPTPortBase ; Get parallel port base I/O address
- inc dx
- inc dx ; Point to control register
- mov al,090h ; Timer 2, lobyte only, mode 0, binary
- pushf
- cli ; No interrupts
- out 43h,al ; Send command byte - prepare the CTC
- in al,dx ; Get parallel port value
- and al,11111110b ; Clear bit 0 (set pin 1, -STROBE, high)
- mov ah,al ; To AH for later
- inc ax ; Set bit 0 (set pin 1, -STROBE, low)
- out dx,al ; Set the I/O register
- ; At this point the -STROBE pin goes low
- mov al,NCTCClocks ; Number of CTC clocks to wait
- out 42h,al ; Start the timer
- in al,61h ;!! Use 62h for PC and XT!!
- WaitCTC: in al,61h ;!! Use 62h for PC and XT!!
- test al,20h ; Test bit 5 - has the time expired?
- jz WaitCTC ; If not, loop
- mov al,ah ; Get value with bit 0 off
- out dx,al ; Write it
- ; At this point the -STROBE pin returns high
- popf ; Restore interrupt flag
- -------------------------------- snip snip snip --------------------------------
-
- Note that CTC channel two is being used in lobyte-only mode for maximum access
- speed; if you need to delay more than 255 CTC clocks, use the timer in the
- lobyte-hibyte mode and write a two-byte reload register value.
-
- You would want to avoid using CTC channel 2 for audio generation, including
- the standard BIOS beep, if you were using this technique inside an interrupt
- handler, because any beep in progress will be cut off when this code executes.
-
- {JAM} says: "On reasonably fast machines, timer 2 can be used to create delays
- from 5 to 54,000 microseconds with 1 to 2 microsecond accuracy".
-
- ## 7.32 TIMING SHORT PERIODS USING MODE THREE
-
- For timing short periods, where an absolute timestamp is not required, a
- simplified technique can be used, using CTC channel zero in mode three.
-
- Traditionally the BIOS programmed CTC channel zero to operate in mode three
- with a Reload value of zero. Modern BIOSes seem to prefer to use mode two.
- See section »» 7.4.2 for details.
-
- Referring to sections »» 7.8.5, »» 7.15.2 and »» 7.20, in mode three, the raw
- value read from the count register decrements in steps of two, each step
- corresponding to one 0.8381 us CTC clock period. Therefore, periods of time
- comfortably less than about 27 ms can be measured by reading the counter,
- storing the value read, then repeatedly reading the counter, calculating the
- difference, and waiting for this difference to exceed the desired number, which
- will be twice the number of 0.8381 us periods (because the timer decrements in
- steps of two).
-
- This technique is demonstrated in the sample program in section »» 7.34.
-
- ## 7.33 VERTICAL RETRACE
-
- A video monitor display is created by the electron beam in the monitor (colour
- monitors have three) which scans the screen in a 'raster' fashion similar to
- the way you read a book (though a lot quicker :-) The beam starts at the top
- left corner and draws one line from left to right, then returns to the left and
- scans the line below, and so on until the entire screen has been scanned. Each
- of these horizontal scans is called a line, and at least 15000 lines are scanned
- each second (depending on the screen resolution and timing parameters).
-
- Each time the electron beam reaches the right side of the screen, it returns
- very quickly to the left side of the screen, ready to scan the next line. This
- short 'return' time is called the horizontal retrace. The horizontal retrace
- interval is very short (a few microseconds) and is significant on some old CGA
- cards because a 'snow' effect was produced unless the video buffer was only
- accessed during the horizontal retrace interval.
-
- After the full screen has been scanned, the beam turns off and returns to the
- top of the screen. This is the vertical retrace interval, which occupies a
- length of time in the order of one or two milliseconds (depends on video mode
- timing parameters), and occurs about 50 to 70 times per second (equal to the
- field rate, or vertical scan rate, sometimes called the 'refresh' rate).
-
- LCD displays are not physically scanned in the same way, but they usually get
- their display information from a signal which is raster oriented. In any case,
- vertical retrace is emulated on LCD machines, for compatibility.
-
- Vertical retrace is indicated by a status bit in the video status register at
- I/O location 3BA hex (MDA, Hercules, and EGA and VGA in monochrome modes) or
- 3DA hex (CGA, MCGA, and EGA and VGA in colour modes).
-
- For CGA, MCGA, EGA, and VGA cards, bit 3 indicates vertical retrace, and is
- set during the retrace interval (i.e. clear during the display period) except
- for the MCGA card in 640x480 monochrome mode, when the bit has the opposite
- polarity (although the status register appears at 3DA, the colour address!).
-
- The MDA card does not have a vertical retrace indication, though the Hercules
- card does indicate vertical sync on bit 7 of the register at 3BA, with opposite
- polarity, i.e. the bit is clear during retrace).
-
- Some video cards are also able to generate IRQ2/9 on vertical retrace but
- standard VGA cards do not have this facility, so I will not describe it here.
- This interrupt can be simulated fairly successfully using CTC channel 0. This
- technique is described in section »» 10.16.
-
- The word at low memory address 0040:0063 (or 0000:0463) contains the I/O
- address of the CRTC which can be used to determine whether the video system
- is colour or monochrome. A value of 3B4 hex indicates monochrome. In this
- case vertical retrace detection is unreliable, as the MDA does not have any
- vertical retrace indication. A value of 3D4 hex indicates colour, in which
- case vertical retrace is indicated by bit 3 in the register at I/O address
- 3DA hex.
-
- ## 7.34 SAMPLE PROGRAM: TIMING SHORT PERIODS USING MODE THREE
-
- The following program uses CTC channel 0 in mode three to measure short
- durations to provide a striped background colour on a VGA adapter. It uses
- the VGA vertical retrace signal to synchronise the time periods with the
- start of each screen update.
-
- The effect of turning the computer's turbo switch on and off is minimal, and
- is not cumulative; this demonstrates that the program is correctly using the
- CTC to measure the time delay.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #9
- Demonstrates timing short periods using mode three
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE9.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample9.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <dos.h> /* Needed for MK_FP() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define DELAY 1790 /* Delay 1790 x 0.8381 us = 1.5 ms */
-
- #define DAC_ADDR 0x3C8 /* VGA DAC address register */
- #define DAC_DATA 0x3C9 /* VGA DAC data register (write) */
-
- #define BIOSSHIFT (*((unsigned char far *)MK_FP(0x40, 0x17)))
-
- unsigned int read_timer0_mode3_raw(void) {
- asm pushf;
- asm xor al,al;
- asm cli;
- asm out 43h,al;
- asm in al,40h;
- asm mov ah,al
- asm in al,40h
- asm popf;
- asm xchg al,ah;
- return _AX; /* Return raw value */
- }
-
- void main(void) {
- unsigned int video_status; /* I/O address of video status reg */
- unsigned int colour[3]; /* Background colour - R, G, and B */
- unsigned int rgbsel; /* Selects which colour to change */
- unsigned int ctcval, newctc; /* Raw mode three values */
-
- printf("Sample program #9 - Demonstrates timing short periods using mode three\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Press the Ctrl key to exit\n");
-
- video_status = *((unsigned int far *)MK_FP(0x40, 0x63)) + 6;
-
- /* First, make sure it's in mode three! */
-
- asm cli;
- outportb(0x43, 0x36);
- outportb(0x40, 0);
- outportb(0x40, 0);
- asm sti;
-
- /* Wait for vertical retrace to start */
-
- while ((inportb(video_status) & 0x08) == 0)
- ;
-
- /* Start of retrace - reset colours */
-
- newscr:
- colour[0] = colour[1] = colour[2] = 0;
- rgbsel = 0;
-
- asm cli;
- outportb(DAC_ADDR, 0);
- outportb(DAC_DATA, 0);
- outportb(DAC_DATA, 0);
- outportb(DAC_DATA, 0);
- asm sti;
-
- /* Check for CTRL pressed and terminate if so */
-
- if (BIOSSHIFT & 0x04) {
- while (bioskey(1))
- bioskey(0); /* Flush buffer */
- exit(0);
- }
-
- /* Wait for start of display */
-
- while ((inportb(video_status) & 0x08) != 0)
- ;
-
- /* Get the time now */
-
- ctcval = read_timer0_mode3_raw();
-
- /* Loop waiting for nominated time period to elapse, check for end of display */
-
- while (1) {
- do {
- if ((inportb(video_status) & 0x08) != 0)
- goto newscr; /* If retrace has started */
- newctc = read_timer0_mode3_raw(); /* Sample the time */
- newctc = ctcval - newctc; /* Get CTC clocks elapsed x2 */
- }
- while (newctc < (DELAY * 2)); /* Loop until desired time */
-
- /* Time has elapsed - bump time reference and change the background colour */
-
- ctcval -= (DELAY * 2); /* Use * 2 because of mode 3 */
-
- colour[rgbsel] += 22; /* Increase R/G/B component */
- if (++rgbsel > 2) /* Change R/G/B selector */
- rgbsel = 0;
-
- asm cli;
- outportb(DAC_ADDR, 0);
- outportb(DAC_DATA, colour[0]);
- outportb(DAC_DATA, colour[1]);
- outportb(DAC_DATA, colour[2]);
- asm sti;
- } /* for */
- } /* main() */
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.35 THE REAL TIME CLOCK (RTC)
-
- The RTC/RAM chip is a Motorola MC146818A chip or workalike. It is not present
- in the original PC and XT and may not be present in non-hardware-compatible
- machines. It is often implemented as part of an ASIC, or in a hybrid module
- such as the DS1287, which contains the RTC/RAM chip, crystal, and backup
- battery.
-
- It is a CMOS device, containing a crystal oscillator and divider with interrupt
- and alarm logic, a non-volatile CMOS RAM array which stores the BIOS parameter
- settings, and a processor interface based on the CMOS RAM register file (which
- contains 64 or sometimes 128 registers).
-
- The crystal oscillator normally operates at 32768 Hz, using a small watch type
- crystal. The RTC has an interrupt output, which is wired to IRQ8 (normally
- mapped to int 70h). The RTC is accessed at I/O addresses 70 hex and 71 hex.
- Both ports are 8-bit and should only be accessed using 8-bit I/O instructions.
-
- The port at 70h is the address select port, which selects which of the 64 or
- 128 internal registers will be addressed by an access to I/O address 71h.
- The original MC146818 chip is bus-addressable, and this address/data system
- may be implemented in logic on the motherboard, not on the RTC/RAM chip itself.
- After the register has been specified by writing a register number to port 70h,
- the selected register's contents can then be read or written via the port at
- I/O address 71 hex. This address register and data register technique reduces
- the amount of I/O space required by the RTC, and is not actually part of the
- MC146818, but is implemented on the motherboard or in the ASIC. The same
- technique is used in the CRT Controller chip and other chips on video cards.
-
- Always disable interrupts around the access sequence, otherwise an interrupt
- routine could select a different RTC register, causing your code to read or
- write the wrong register. Also, note that the address select register at I/O
- address 70h is write-only. Reading the register will yield an undefined value.
-
- ## 7.35.1 READING AND WRITING RTC REGISTERS
-
- Here are functions to read and write RTC registers. Inline assembler is
- required for pushf and popf. See section »» 6.22 for the explanation of
- the pushf/cli/popf technique. The cli could be replaced by the disable()
- pseudofunction. Change inportb() and outportb() to inp() and outp() for
- Microsoft C, I think.
-
- -------------------------------- snip snip snip --------------------------------
- unsigned char read_rtc_register(unsigned char reg_num) {
- unsigned char rv;
- asm pushf;
- asm cli;
- outportb(0x70, reg_num);
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- rv = inportb(0x71);
- asm popf;
- return rv;
- }
-
- void write_rtc_register(unsigned char reg_num, unsigned char value) {
- asm pushf;
- asm cli;
- outportb(0x70, reg_num);
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- outportb(0x71, value);
- asm popf;
- return;
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.35.2 ALLOCATION OF THE RTC REGISTERS
-
- The first 10 registers (registers 0 to 9) are the date and time registers
- (including the alarm settings). These registers cannot be accessed during the
- update period, which is approximately two milliseconds long and occurs every
- second (details are given later). Registers 10 to 13 are control registers.
- The remaining registers (14-63 on a standard MC146818 which has 64 registers,
- or 14-127 on an enhanced version) are general purpose CMOS RAM locations, which
- are used by the BIOS to store setup information, and do not relate to timing.
-
- The time and date values are configurable for either packed BCD or binary data
- format, but the BIOS uses the packed BCD format, and some workalike chips do
- not support binary format, so for practical purposes, packed BCD format is
- mandatory. See the glossary for a description of packed BCD.
-
- Important! The date and time registers (registers 0 to 9) will yield correct
- values only if no update is in progress. See notes on Register A for details.
- These registers should not be written unless the 'Set' bit in Register B is
- set. See notes on Register B for details.
-
- The registers are as follows:
-
- Reg Function Format Range
- --- -------- ------ -----
-
- 0 Seconds Two digit packed BCD 0 to 59
- 1 Seconds alarm Two digit packed BCD 0 to 59
- 2 Minutes Two digit packed BCD 0 to 59
- 3 Minutes alarm Two digit packed BCD 0 to 59
- 4 Hours See below
- 5 Hours alarm See below
- 6 Day of week BCD 1 to 7 (see below)
- 7 Date of month Two digit packed BCD 1 to 31
- 8 Month Two digit packed BCD 1 to 12
- 9 Year Two digit packed BCD 0 to 99
- 10 Register A See below
- 11 Register B See below
- 12 Register C See below
- 13 Register D See below
-
- The hours and hours alarm registers (registers 4 and 5) are formatted in
- 12-hour or 24-hour mode, depending on the setting of bit 1 of Register B
- (see the description for this bit). In 12-hour mode, bits 6-0 of the
- hours registers are the hours value, in the range 1 to 12, and bit 7 is
- the PM indicator (set indicates PM). In 24-hour mode, bits 7-0 of the
- hours registers are in 24-hour format (range 0 to 23).
-
- The seconds alarm, minutes alarm, and hours alarm registers may be set to a
- value from 0C0 hex to 0FF hex to indicate 'don't care'. For example if the
- seconds alarm value is zero, the minutes alarm value is 30 (stored in packed
- BCD form, of course), and the hours alarm value is 0FF hex, the alarm will
- be signalled at half past every hour. Note that this 'don't care' function
- may not be implemented in all ASIC workalikes.
-
- The day of week register (register 6) simply counts 1, 2, 3, 4, 5, 6, 7, 1,
- 2... where 1 means Sunday, 2 means Monday, etc. The RTC does not calculate
- the day of the week from the date. This register must be set by software.
- It is not used by the BIOS RTC functions or by DOS and will not necessarily
- be set correctly. Software normally calculates the day of week from the other
- date information rather than using this register. The RTC uses this register
- to switch between standard time and daylight saving time if daylight saving is
- enabled, but the daylight saving function is not used in PCs so there is no
- need to make sure that this register is set correctly.
-
- ## 7.35.3 RTC REGISTER A
-
- Register A is register number 10. It is read/write except bit 7, which is
- read-only:
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . Update In Progress (UIP) flag
- . * * * . . . . Prescaler control bits
- . . . . * * * * Periodic Interrupt rate control
-
- The UIP flag, if set, indicates that an update is in progress or is imminent.
- An update occurs once every second and takes approximately two milliseconds.
- During the update period, the values read from the date and time registers
- (though not the alarm registers) are changing and are not valid, because the
- RTC chip operates quite slowly internally (being low power CMOS) and it takes
- a while for an update to 'ripple through' from the seconds register all the
- way up to the year register.
-
- If the UIP flag is set, the date and time registers (registers 0-9) should not
- be accessed. Software must wait until the UIP flag becomes clear before reading
- any time or date related registers. The UIP flag becomes active approximately
- 244 us prior to the start of the update cycle, therefore the read or write
- operation must take less than 244 us to ensure that it completes before the
- update cycle begins.
-
- The Prescaler control bits determine what crystal frequency the RTC expects,
- and allow the prescaler and divider to be held reset. The values are:
-
- bit 6 5 4
-
- 0 0 0 Operation with 4.194304 MHz crystal
- 0 0 1 Operation with 1.048576 MHz crystal
- 0 1 0 Operation with 32768 Hz crystal (default)
- 0 1 1 Undefined
- 1 0 x Undefined
- 1 1 x Hold prescaler and divider reset (stops counting)
-
- (x means don't-care)
-
- While the prescaler and divider are held reset, counting and updating ceases.
- The first update will occur half a second after this condition is removed.
-
- The Periodic Interrupt rate control bits determine the periodic interrupt
- rate (d'oh :-) Here are the values:
-
- bit 3 2 1 0 Period Ints per second
-
- 0 0 0 0 No periodic interrupt
- 0 0 0 1 3.90625 ms 256 (see note below)
- 0 0 1 0 7.8125 ms 128 (see note below)
- 0 0 1 1 122.0703125 us 8192
- 0 1 0 0 244.140625 us 4096
- 0 1 0 1 488.28125 us 2048
- 0 1 1 0 976.5625 us 1024 (BIOS default)
- 0 1 1 1 1.1953125 ms 512
- 1 0 0 0 3.90625 ms 256
- 1 0 0 1 7.8125 ms 128
- 1 0 1 0 15.625 ms 64
- 1 0 1 1 31.25 ms 32
- 1 1 0 0 62.5 ms 16
- 1 1 0 1 125 ms 8
- 1 1 1 0 250 ms 4
- 1 1 1 1 500 ms 2
-
- Note: Combinations 0001 and 0010 duplicate 1000 and 1001 respectively.
- If the RTC is operating from a 1MHz or 4MHz crystal (prescaler control bits
- are 00x), combinations 0001 and 0010 give interrupt rates of 30.517578125 us
- (32768 interrupts per second) and 61.03515625 us (16384 interrupts per second)
- respectively. 1MHz and 4MHz crystals are not used with RTCs in PCs because
- of the increased power consumption.
-
- ## 7.35.4 RTC REGISTER B
-
- Register B is register number 11. It is fully read/write:
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . Set flag (1 = set mode)
- . * . . . . . . PIE, Periodic Interrupt Enable (1 = enable)
- . . * . . . . . AIE, Alarm Interrupt Enable (1 = enable)
- . . . * . . . . UIE, Update Interrupt Enable (1 = enable)
- . . . . * . . . SQWE, Square wave enable, not used in PCs
- . . . . . * . . DM, BCD/binary mode (1 = binary)
- . . . . . . * . 12/24-hour mode (0 = 12-hour, 1 = 24-hour)
- . . . . . . . * DSE, Daylight Saving Enable (1 = enable)
-
- The Set flag must be set by software before any real-time registers (current
- date and time) are modified. When the bit is set, any real-time register
- update in progress is aborted, and while the bit is set, updates are prevented
- and the UIP bit in Register A remains clear. After setting the real-time
- registers, the SET bit must be cleared to resume normal operation.
-
- The PIE, AIE, and UIE enable the periodic, alarm, and update interrupts
- respectively, if set. The periodic interrupt occurs regularly as defined by
- bits 0-3 of Register A. The update interrupt, if enabled, occurs every second,
- immediately following an update. The alarm interrupt occurs whenever the
- hours, minutes and seconds registers match the time programmed into the alarm
- registers. See the note after the register list for alarm register details.
-
- The SQWE bit enables the square wave output at the frequency set by bits 0-3 of
- Register A. This pin is not used in PC applications.
-
- If the 12/24-hour mode is changed, the hours register should be reprogrammed.
-
- Daylight Saving mode, if enabled, causes the time to jump forward from 01:59:59
- to 03:00:00 on the morning of the last Sunday in April, and backward from
- 01:59:59 to 01:00:00 on the last Sunday in October. The day of week register
- must be set correctly for this to work properly. The PC does not use this
- function.
-
- ## 7.35.5 RTC REGISTER C
-
- Register C is register number 12. It is read-only; writes are ignored.
- It contains three interrupt source flags and the combined interrupt flag.
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . IRQF (combined interrupt flag)
- . * . . . . . . PF (periodic flag)
- . . * . . . . . AF (alarm flag)
- . . . * . . . . UF (update flag)
- . . . . * * * * Unused; zero, read-only
-
- The three interrupt source flags are set if the condition that would generate
- the interrupt has occurred, regardless of whether the interrupt source is
- enabled (via Register B). These can be used to permit software polling of
- these conditions, if generating an actual interrupt is not justified.
-
- Any active interrupt source flags are cleared immediately after reading this
- register; thus if several interrupt sources are active, the software must be
- careful to check for each possible interrupt flag after every read of this
- register, otherwise a signal may be missed.
-
- The IRQF flag (combined interrupt flag) is set if the interrupt output from
- the RTC chip is active. This will be true if any of the interrupt source
- flags in this register are set in conjunction with that interrupt source being
- enabled via Register B.
-
- ## 7.35.6 RTC REGISTER D
-
- Register D is register number 13. Only bit 7 is meaningful, and this bit is
- read-only.
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . VRT, Valid Ram and Time flag
- . * * * * * * * Unused; zero, read-only
-
- The VRT flag indicates whether a power-up has occurred. It is cleared during
- loss of supply voltage, and is set immediately after a read of Register D.
-
- ## 7.35.7 READING THE RTC
-
- When reading the RTC's real-time registers it is necessary to avoid reading
- them during the update period, during which time they cannot be accessed by
- the processor (reading registers will yield undefined values, and writes will
- be ignored). Registers A, B, C, and D can be accessed at any time.
-
- Your software can use the UIP flag bit in Register A to determine whether an
- update is in progress or imminent. If this flag is clear, your software then
- has up to 244 us in which to perform the desired register access(es), and may
- then re-check the UIP flag and make more accesses if appropriate.
-
- If the UIP flag is set, the software may have to wait up to approximately 2.25
- ms before the UIP flag is clear. If such a long delay in a read-RTC function
- is undesirable, a possible solution in some cases could be to store the time
- each time the RTC is read, and if the RTC is not available due to an update
- cycle being in progress, return the most recently read RTC value instead.
-
- Alternatively, the Update Interrupt and/or the Update Flag in Register C can
- be used to schedule reads of the RTC so they occur immediately after an update,
- either under interrupt (if the RTC interrupt is not required for any other
- purpose), or by polling the Update Flag and reading the real-time registers
- as soon as the flag reads as '1' (assuming no long background processes are
- active, this gives the code almost a whole second to make its RTC accesses
- before the next update cycle will begin.
-
- ## 7.35.8 SAMPLE PROGRAM: A TSR CLOCK USING INT 8 AND THE RTC
-
- This program is a TSR which hooks interrupt 8 and uses the RTC to display a
- persistent HH:MM:SS format time in the top right corner of the screen.
-
- -------------------------------- snip snip snip --------------------------------
- NAME SAMPLE10
-
- ; Sample program #10
- ; Demonstrates a TSR clock using int 8 and reading the RTC directly
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This program assembles into SAMPLE10.COM, a TSR program which displays the
- ; current time in the top right corner of the screen in text modes. It uses
- ; int 8 to get execution 18.2065 times per second, reads the RTC time directly
- ; from the RTC chip, and updates the screen every second. This program does
- ; not attempt to ascertain that an RTC is present. Also, it has no uninstall
- ; facility.
- ;
- ; If a non-standard video mode (i.e. mode 14 hex or higher) is in use, this
- ; program will assume that it is a text mode. This will probably result in
- ; disturbance to the display in high resolution graphics modes. This program
- ; is intended to be instructional only.
- ;
- ; Save this file to SAMPLE10.ASM and assemble with:
- ; masm SAMPLE10;
- ; link SAMPLE10;
- ; exe2bin SAMPLE10.exe SAMPLE10.com
- ; or
- ; tasm SAMPLE10;
- ; tlink /t SAMPLE10;
- ;
-
- ComFile SEGMENT
- ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing
-
- ORG 100h ; COM-type file
-
- Main PROC near
- jmp Main2
- Main ENDP
-
- Hours DB 0FFh ; Hours (BCD) of last update
- Minutes DB 0FFh ; Minutes (BCD) of last update
- Seconds DB 0FFh ; Seconds (BCD) of last update
-
- ASSUME ds:nothing
-
- ; The following function handles int 8, the timer tick interrupt. It reads
- ; Register A and checks for an update in progress, and skips if so. It
- ; then reads the seconds register and checks to see whether he seconds have
- ; changed. If they have, it reads the hours and minutes registers also, and
- ; then displays the current time in HH:MM:SS format in the top right hand
- ; corner of the screen if the screen is currently in text mode.
- ;
- ; This procedure calls no DOS or BIOS functions.
- ;
- ; Note that the whole routine, and also the DisplayTime subroutine which is
- ; called by this routine, and its subroutines, run with interrupts disabled.
-
- NewInt08 PROC far ; Int 8 intercepter
- pushf ; Preserve flags
- push ax ; Keep register
- cli ; Just in case
- mov al,0Ah ; Register number for register A
- out 70h,al ; Set it
- jmp SHORT $+2 ; Delay
- in al,71h ; Read register
- and al,80h ; Test update-in-progress flag
- jnz Chain08 ; If busy, do it next time
- jmp SHORT $+2 ; Delay
- out 70h,al ; Select register zero - seconds
- jmp SHORT $+2 ; Delay
- in al,71h ; Read seconds
- cmp al,Seconds ; Did seconds change?
- je Chain08 ; If not, do nothing on this interrupt
- mov Seconds,al ; Store new seconds
- mov al,2 ; Minutes register
- jmp SHORT $+2 ; Delay
- out 70h,al ; Set it
- jmp SHORT $+2 ; Delay
- in al,71h ; Get minutes
- mov Minutes,al ; Store
- mov al,4 ; Hours register
- jmp SHORT $+2 ; Delay
- out 70h,al ; Set it
- jmp SHORT $+2 ; Delay
- in al,71h ; Get hours
- mov Hours,al ; Store
- call DisplayTime ; Display the time
- Chain08: pop ax ; Restore
- popf ; Restore
- DB 0EAh ; JMP xxxx:yyyy
- Old08Ofs DW 0 ; Vector to original handler - Offset
- Old08Seg DW 0 ; Segment
- NewInt08 ENDP
-
- ; This procedure uses the time values stored in Hours, Minutes, and Seconds to
- ; create a time value in the form HH:MM:SS and stores this to the top right
- ; corner of the screen. It checks the current video mode to avoid overwriting
- ; video memory incorrectly when a graphics mode is active, and also supports
- ; non-standard screen resolutions. It does not support Hercules graphics mode,
- ; because there is no standard video mode number for this mode as it is not
- ; officially recognised by IBM.
-
- DisplayTime PROC near ; Display the current time
- push ds ; Need DS
- xor ax,ax ; Zero
- mov ds,ax ; Address BIOS vars using DS
- mov al,ds:[449h] ; Get video mode
- cmp al,4 ; Check for modes 0-3 (text)
- jb TextMode ; If so
- cmp al,7 ; Check for MDA text mode
- je TextMode ; If so
- cmp al,14h ; Check for last standard graphics mode
- jb GraphMode ; If graphics, otherwise assume text
- TextMode: push bx ; Keep BX
- mov al,ds:[484h] ; Get number of lines minus one
- inc ax ; Get number of lines (not minus one)
- mov ah,ds:[462h] ; Get active page
- mul ah ; Get starting line number
- add ax,ds:[44Ah] ; Add number of columns
- shl ax,1 ; Get offset of end of line
- sub ax,18 ; Back up to 9 chars back from end
- xchg ax,bx ; To BX
- cmp BYTE PTR ds:[463h],0B4h ; Check for monochrome
- mov ax,0B000h ; Prepare for monochrome
- je HaveRegen ; If so
- mov ah,0B8h ; If not, use CGA regen buffer
- HaveRegen: mov ds,ax ; Address regen buffer with DS
- mov WORD PTR ds:[bx-2],720h ; Space before time
- mov al,Hours ; Get hours
- and al,7Fh ; Mask off AM/PM bit
- call StoreBCD ; Convert BCD to ASCII and store
- mov al,Minutes ; Get minutes
- call StoreColonBCD ; Convert BCD to ASCII and store
- mov al,Seconds ; Get seconds
- call StoreColonBCD ; Convert BCD to ASCII and store
- mov WORD PTR ds:[bx],720h ; Space after time
- pop bx ; Restore BX
- GraphMode: pop ds ; Fix up
- ret ; Done
- DisplayTime ENDP
-
- StoreColonBCD PROC near
- mov WORD PTR ds:[bx],":"+700h ; Colon (with attribute)
- inc bx
- inc bx ; Bump pointer
- StoreBCD PROC near
- push ax ; Keep
- shr al,1
- shr al,1
- shr al,1
- shr al,1
- call StoreBCDChar
- pop ax
- and al,0Fh
- StoreBCDChar PROC near
- add al,"0"
- mov ah,7
- mov ds:[bx],ax
- inc bx
- inc bx ; Bump pointer
- ret
- StoreBCDChar ENDP
- StoreBCD ENDP
- StoreColonBCD ENDP
-
- Discard EQU $ ; Discard point
- TSRParas = (OFFSET (Discard-@curseg+15) SHR 4)
-
- ASSUME ds:ComFile
-
- SignOnMsg DB "Sample program #10 - TSR clock using int 8 and direct RTC access",13,10
- DB "Part of the PC Timing FAQ / Application notes",13,10
- DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10
- DB "Installed",13,10,13,10,"$"
-
- ; Check DOS version
-
- Main2 PROC near
- mov ah,30h
- int 21h
- cmp al,2 ; Expect DOS 2.0 or later
- jae DOS_Ok
- int 20h
-
- ; Intercept int 8
-
- DOS_Ok: mov ax,3508h
- int 21h ; Get vector for int 8
- mov [Old08Ofs],bx
- mov [Old08Seg],es ; Store it
-
- mov dx,OFFSET NewInt08
- mov ax,2508h
- int 21h ; Set new vector
-
- mov es,ds:[2Ch] ; Get segment of environment block
- mov ah,49h
- int 21h ; Deallocate our copy of environment
-
- mov dx,OFFSET SignOnMsg
- mov ah,9
- int 21h ; Display message
-
- mov dx,TSRParas ; Number of paragraphs to leave resident
- mov ax,3100h
- int 21h ; Go resident
- Main2 ENDP
-
- ComFile ENDS
- END Main
- -------------------------------- snip snip snip --------------------------------
-
- This program demonstrates why interrupts must be locked out during manipulation
- of hardware devices such as the RTC, because this TSR's int 8 handler explicitly
- changes the address register in the RTC on each timer tick. If some foreground
- code set the address register then made an access to the data register, without
- disabling interrupts around the sequence, very occasionally an int 8 could be
- signalled between the address register access and the data register access,
- causing an incorrect value to be read or causing the time to change to a random
- or meaningless value. This type of bug could be almost impossible to track
- down. This is why it is important to follow these guidelines, though programs
- that do not follow these guidelines often appear to work correctly.
-
- This is also a good example of a TSR which lengthens processing of int 8 and
- also lengthens this processing unevenly; most int 8 calls will be lengthened
- only slightly, but every time the seconds change, the int 8 invocation will be
- lengthened by a much greater amount. See section »» 6.16.1.
-
- ## 7.36 THE RTC INTERRUPT AND RELATED BIOS FUNCTIONS
-
- The RTC interrupt is IRQ8, which maps to int 70 hex. It does not exist on the
- PC and XT. This interrupt is invoked when any enabled interrupt source in the
- RTC issues an interrupt, providing that IRQ8 is enabled in the secondary PIC's
- Interrupt Mask Register (section »» 6.10) and IRQ2, the cascade interrupt, is
- enabled in the primary PIC's IMR. See section »» 7.35.4 for info on how to
- enable and disable the four interrupt sources in the RTC.
-
- Usually, only the Alarm and the Periodic Interrupt triggers on the RTC are ever
- used. The RTC interrupt is used in three ways:
-
- ■ The 24-hour Alarm signal from the RTC (see section »» 3.4),
- ■ The Event Wait and Delay functions of the BIOS (section »» 7.36.1),
- ■ User programs (e.g. slow-down programs or programs that measure
- the execution time of other programs).
-
- The Alarm signal uses the Alarm function of the RTC (obviously), and this
- interrupt source is only enabled if the appropriate BIOS function has been
- called to enable the alarm function.
-
- The other two uses of int 70h involve the periodic interrupt from the RTC,
- which is operated at 1024 interrupts per second (see section »» 7.35.3),
- giving an interrupt every 976.5625 microseconds. This interrupt source on the
- RTC is only enabled when the BIOS Event Wait or Delay functions are requested,
- unless explicitly enabled by a foreground program or TSR directly accessing
- the RTC registers.
-
- IRQ8 (int 70h) must also be enabled in the PIC IMR. Often it will be left
- enabled, and the various interrupt sources will be controlled at the RTC, but
- some BIOSes may also disable the interrupt level when it is no longer required.
-
- ## 7.36.1 THE BIOS EVENT WAIT AND DELAY FUNCTIONS
-
- On AT and later machines, the BIOS provides an 'event wait' function and a
- delay function, that use the RTC interrupt for timing.
-
- The 'event wait' and delay functions use nine bytes of RAM in the BIOS data
- area, which are defined as follows:
-
- Address Type Description
-
- 0040:0098 Far ptr Pointer to byte to be set to 80h when event
- wait completes
- 0040:009C DWord Counter (down-counter, microseconds)
- 0040:00A0 Byte Status:
- 00h = Idle
- 01h = Event Wait or Delay in progress
- 80h = Delay time elapsed (transitional)
-
- The functions are as follows.
-
- Set Event Wait : int 15h
- Call with: AX = 8300 hex
- CX = Time to wait (microseconds) hiword
- DX = Time to wait (microseconds) loword
- ES:BX = Pointer to flag to be set when complete
- Returns: CF = Error indication (see below)
-
- This function sets up control information in the BIOS data area, and starts an
- 'event wait' timeout by enabling IRQ8 (int 70h) in the PIC and enabling the
- periodic interrupt via the RTC. It returns to the caller while the event wait
- is in progress. The event wait is counted down in the background, by the
- BIOS's IRQ8 (int 70h) handler. When the specified time elapses, the interrupt
- handler sets the byte that was pointed to by ES:BX when the function was
- invoked, to 80h, and the wait is complete.
-
- If this function is called when an event wait or delay function (described
- later) is in progress, it will return with carry set and ignore the request.
- If the function is not supported by the BIOS, it will return with carry set
- and AH set to 80h or 86h.
-
- Cancel Event Wait : int 15h
- Call with: AX = 8301 hex
- Returns: CF = Error indication
-
- This function cancels the event wait currently in progress. It disables the
- periodic interrupt in the RTC and resets the event wait status byte at
- 0040:00A0 to zero.
-
- Delay : int 15h
- Call with: AH = 86 hex
- CX = Time to delay (microseconds) hiword
- DX = Time to delay (microseconds) loword
- Returns: CF = Error indication
-
- This function delays for the number of microseconds specified in CX and DX, and
- returns with carry clear when the delay is complete. It returns with carry set
- and AH set to 80h or 86h if the function is unsupported. It returns with carry
- set if the delay function was called while an Event Wait or another Delay was
- in progress.
-
- This function uses the same data structure as the Event Wait function, but sets
- the pointer that determines the byte to be set to 80h when the wait completes,
- to point to the status byte.
-
- The time for these functions is specified in microseconds, but the resolution
- is only 977 us, since timing is done using the RTC interrupt. The periodic
- interrupt in the RTC is not resynchronised by the function, so there is also
- a 977 us uncertainty at the start of the time period, which limits accuracy of
- short delays. Also, IRQ8 (int 70h) occurs every 976.5625 us but the handler
- subtracts 977 from the count each time. This is an error of 0.0448% (448 ppm,
- 38.71 seconds per day). This error is cumulative and could become significant
- on long delays (it will make the delay shorter than expected).
-
- If any software locks out interrupts for more than 977 us at a time, interrupts
- will be missed and the time period will be extended. The BIOS joystick reading
- function (section »» 10.4.2) and the joystick position reading function given
- in section »» 10.4.4 may cause this problem.
-
- I have heard that the Event Wait function is used by the hard disk and floppy
- disk BIOS code, but I don't know the details. Info is welcomed. (*)
-
- ## 7.36.2 THE BIOS RTC INTERRUPT HANDLER
-
- The BIOS has its own IRQ8 (int 70h) handler, which counts down the Event Wait
- or Delay time value and sets the flag byte to 80h when the time expires (see
- section »» 7.36.1 for the gory details). The handler makes use of the three
- variables in the BIOS data area which are described in section »» 7.36.1.
- The exact behaviour may vary from one BIOS to another but is something like:
-
- First, check whether an alarm interrupt occurred (using Register C of the RTC,
- see section »» 7.35.5). If so, invoke int 4Ah (see section »» 3.4). Then
- check whether a periodic interrupt occurred. If so, subtract 977 from the
- unsigned long microsecond counter (see section »» 7.36.1). If this resulted in
- the long variable borrowing (i.e. wrapping around from a small positive number
- to a negative number), zero the status flag (see section »» 7.36.1), disable the
- regular interrupt source in the RTC, then set to 80 hex the byte pointed to by
- the far pointer in the BIOS data area (see section »» 7.36.1 again).
-
- The RTC interrupt handler in some BIOSes may unconditionally turn off the
- periodic interrupt enable in the RTC if the status flag (see section »» 7.36.1
- again) is zero, to avoid unnecessary processor overhead (1024 interrupts per
- second can be significant).
-
- ## 7.36.3 USING THE RTC INTERRUPT
-
- The RTC interrupt is int 70h (IRQ8). When a program uses the RTC interrupt,
- it should chain to the original handler, because the BIOS may be in the middle
- of a Delay or Event Wait. The BIOS's int 70h handler may interfere with your
- program, by turning off the periodic interrupt enable in the RTC, so your int
- 70h handler must re-enable it after calling the BIOS's handler.
-
- The BIOS's handler will also count down the microseconds counter (see sections
- »» 7.36.1 and »» 7.36.2) and when it borrows, will set a memory variable at the
- address pointed to by the pointer in the BIOS data area, to 80 hex. This may
- not be desirable, as this pointer may be uninitialised, or may point to a
- variable in a program that is no longer running, etc. Therefore your program
- should be careful to prevent the BIOS's handler from doing this. This is
- demonstrated in the sample program in section »» 7.36.4.
-
- ## 7.36.4 SAMPLE PROGRAM: USING THE RTC INTERRUPT
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #11
- Demonstrates using the RTC periodic interrupt
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save and assemble the critical error module CRIT_ERR
- Save this sample code to SAMPLE11.C
- Compile this module with:
- bcc -c -I<inc_path> -ms sample11.c
- Link the modules with:
- tlink /c /x <c0_path>\c0s.obj sample11.obj crit_err.obj,
- sample11, nul, <lib_path>\cs
- Where inc_path is the path to your C header files, c0_path is the path to your
- startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <dos.h> /* Needed for inportb(), outportb(), etc */
- #include <io.h> /* Needed for _write() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDERR 2 /* DOS handle for standard error */
-
- static unsigned long rtcticks; /* Counter for RTC interrupts */
-
- void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
- unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
-
- typedef void interrupt (far *intfuncp)(); /* Pointer to an int handler */
-
- intfuncp old_int70 = (intfuncp)0xFFFFFFFFL;
-
- unsigned char read_rtc_register(unsigned char reg_num) {
- unsigned char rv;
- asm pushf;
- asm cli;
- outportb(0x70, reg_num);
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- rv = inportb(0x71);
- asm popf;
- return rv;
- }
-
- void write_rtc_register(unsigned char reg_num, unsigned char value) {
- asm pushf;
- asm cli;
- outportb(0x70, reg_num);
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- asm jmp SHORT $+2
- outportb(0x71, value);
- asm popf;
- return;
- }
-
- void enable_rtc_int(void) {
- asm pushf;
- asm cli;
- write_rtc_register(0x0B, read_rtc_register(0x0B) | 0x40);
- outportb(0xA1, inportb(0xA1) & 0xFE);
- outportb(0x21, inportb(0x21) & 0xFB);
- asm popf;
- return;
- }
-
- void interrupt int70_handler(void) {
- if (read_rtc_register(0x0C) & 0x40)
- ++rtcticks; /* Increment RTC tick counter */
- (old_int70)(); /* Chain to BIOS int 70h handler */
- enable_rtc_int(); /* Make sure RTC int is still enabled */
- if (* ((unsigned int far *)MK_FP(0x40, 0x9E)) > 0xFFFD)
- * (unsigned int far *)MK_FP(0x40, 0x9E) = 0xFFFF;
- return; /* From interrupt */
- }
-
- void abort_cleanup(int dos_is_safe) {
- if (dos_is_safe) {
- if (old_int70 != (intfuncp)0xFFFFFFFFL) {
- setvect(0x70, old_int70);
- old_int70 = (void far *)0xFFFFFFFFL;
- }
- }
- else {
- if (old_int70 != (intfuncp)0xFFFFFFFFL) {
- *((intfuncp far *)MK_FP(0, 0x70 << 2)) = old_int70;
- old_int70 = (void far *)0xFFFFFFFFL;
- }
- }
- return;
- }
-
- void interrupt ctrl_c_handler(void) {
- static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
- if (is_at_crit_prompt())
- abort_cleanup(FALSE);
- else {
- abort_cleanup(TRUE);
- _write(STDERR, &message, sizeof(message));
- }
- exit(255);
- }
-
- void main(void) {
- unsigned long msecs, secs;
- unsigned int partial;
-
- printf("Sample program #11 - Demonstrates using the RTC interrupt\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Press <Esc> to exit\n\n");
-
- crit_err_intercept(); /* Trap critical errors */
- setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
- * (unsigned int far *)MK_FP(0x40, 0x9E) = 0xFFFF;
- old_int70 = getvect(0x70);
- setvect(0x70, int70_handler);
-
- asm cli; /* 1024 interrupts per second */
- write_rtc_register(0x0A, (read_rtc_register(0x0A) & 0xF0) | 0x06);
- asm sti;
- enable_rtc_int();
-
- while (1) {
- asm cli;
- msecs = rtcticks;
- asm sti;
- msecs *= 125; /* Calculate * 125 / 128 */
- msecs >>= 6;
- if (msecs & 1)
- ++msecs; /* Round up */
- msecs >>= 1;
- secs = msecs / 1000;
- partial = msecs % 1000;
- printf("%ld.%03d seconds\r", secs, partial);
- if (bioskey(1))
- if ((bioskey(0) & 0xFF) == 27)
- break;
- }
-
- setvect(0x70, old_int70);
- old_int70 = (void far *)0xFFFFFFFFL;
-
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- The int70_handler() function first checks that this int 70h is caused by the
- periodic interrupt, and if so, it increments its counter. It then calls the
- BIOS int 70h handler unconditionally. The BIOS handler will send the EOI to
- both PICs and will probably turn off the periodic interrupt enable flag in the
- RTC, so this handler turns the periodic interrupt back on. It also tries to
- ensure that no problems will occur with the timeout detection of the BIOS's
- handler. When the long microseconds counter at 0040:009C is decremented below
- zero by the BIOS handler, the BIOS handler writes to a memory variable pointed
- to by the pointer at 0040:0098, and this pointer may not have been initialised.
- By keeping the microsecond count at 0xFFFFxxxx, this routine prevents this
- problem. The mainline also sets the microsecond counter to 0xFFFFxxxx. This
- should allow the Delay and Event Wait functions to be used without interference
- from this program.
-
- ## 7.37 USING CTC CHANNEL ONE AND REFRESH DETECT
-
- My thanks to William Luitje (luitje@m-net.arbornet.org) for introducing me to
- this technique. William reports that it is used by the AMI BIOS during floppy
- disk operations.
-
- As shown in section »» 7.5, bit 4 of Port B at I/O address 61 hex on an AT and
- later machine is a read-only bit carrying a signal called Refresh Detect.
- This signal comes from a 'T' (toggle) flip-flop which is clocked by the refresh
- trigger signal, which comes from CTC channel one. I assume it is used by the
- BIOS POST (Power-on self-test) to check that CTC channel one is functioning
- correctly (IBM's paranoid self-test code has to test every single logic gate
- on the entire motherboard - this from the people who created the error message
- "Keyboard error or no keyboard present - press F1 to continue" :-)
-
- Assuming that the RAM refresh rate has not been changed (see section »» 7.4.3),
- this bit will toggle (change from 0 to 1 or from 1 to 0) once every 15.0857
- microseconds (the exact value is 216/14.31818), and Port B can be polled in a
- loop to implement a delay of any length. For short delays, with interrupts
- locked out, this gives an accurate and very convenient relative delay mechanism.
- However, for long delays, it would be naughty to leave interrupts locked out for
- the entire delay period, and interrupts will cause gaps in the polling process,
- slightly lengthening the delay (it will wait longer than expected).
-
- There are several caveats. This method will not work on PCs and XTs. Also it
- will not work in an environment where Port B is emulated (for example, under
- OS/2 and probably any other virtual DOS machine). Finally, if the DRAM refresh
- period has been changed, the timing will be changed proportionately.
-
- ## 7.37.1 SAMPLE PROGRAM: TIMING THE REFRESH DETECT SIGNAL
-
- This program uses CTC channel 2 to measure a sampling period of half a second
- with interrupts locked out, and counts the number of transitions on the Refresh
- Detect signal during this period. It displays the value after each half-second
- sample, and repeats the sample continuously until Ctrl-C is pressed.
-
- Warnings about using this program: It will not work on an emulated machine,
- i.e. under OS/2 or any other multi-tasking operating system that gives it a
- virtual DOS machine. The program will not run on an old PC or XT; an AT or
- later machine is required. The program will disrupt the DOS time of day, so
- the machine should be rebooted after running this program if that is a problem.
- Also, it does not check the absolute accuracy of the Refresh Detect signal;
- the signal being measured and the sample timer are both derived from the same
- clock source.
-
- The joystick reading sample program in section »» 10.4.4 also demonstrates
- the Refresh Detect signal used as a timing reference.
-
- -------------------------------- snip snip snip --------------------------------
- NAME SAMPLE12
-
- ; Sample program #12
- ; Demonstrates timing the Refresh Detect signal
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This program assembles into SAMPLE12.COM, a small program which measures the
- ; number of DRAM refreshes in a half-second interval. It uses CTC channel 2
- ; in mode 3 to measure half-second sample periods, and counts the number of
- ; transitions on the Refresh Detect signal on Port B bit 4. After each
- ; half-second sample, the value is displayed, and the program repeats itself.
- ; To terminate the program, press any key and wait.
- ;
- ; Save this file to SAMPLE12.ASM and assemble with:
- ; masm sample12;
- ; link sample12;
- ; exe2bin sample12.exe sample12.com
- ; or
- ; tasm sample12;
- ; tlink /t sample12;
- ;
-
- CTC2Divisor = 47727 ; 40ms
- CTC2Toggles = 25 ; Number of CTC channel 2 toggles
- ; expected in half a second
- ComFile SEGMENT
- ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing
-
- ORG 100h ; Com-type file
-
- Main PROC near
- jmp Main2 ; Skip
- Main ENDP
-
- InitialMsg DB "Sample program #12 - Demonstrates timing the Refresh Detect signal",13,10
- DB "Part of the PC Timing FAQ / Application notes",13,10
- DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10,"$"
- NotATMsg DB "Refresh Detect is supported on ATs and later machines; this machine appears",13,10
- DB "to be a PC or XT. The PC and XT do not support Refresh Detect.",13,10,"$"
- ExplanationMsg DB "The numbers displayed are the counts of DRAM refreshes in a 1/2-second sample",13,10
- DB "period. For the standard DRAM refresh rate of 15.0857us, this number should",13,10
- DB "be about 33144. If you have run a program to slow down the DRAM refresh, the",13,10
- DB "numbers will be lower.",13,10,13,10
- DB "To terminate this program, press any key and wait",13,10
- NewlineMsg DB 13,10,"$"
-
- Main2 PROC near
- mov dx,OFFSET InitialMsg ; Opening message
- mov ah,9
- int 21h ; Display it
-
- ; Determine machine type (code from section »» 7.5)
-
- pushf ; Keep interrupt flag
- mov cx,400h ; Six attempts (top bits of CH)
- cli ; Lock out interrupts during this stuff
- in al,61h ; Get Port B contents
- jmp SHORT $+2 ; Short delay
- mov ah,al ; Original value to AH
- Flip61Loop: xor ah,10000000b ; Flip top bit
- mov al,ah ; Get value to AL
- out 61h,al ; Write value to port
- jmp SHORT $+2 ; Short delay
- jmp SHORT $+2 ; Short delay
- in al,61h ; Read it back
- xor al,ah ; Set bit 7 if value didn't stay
- shl al,1 ; Shift bit into carry
- rcl cx,1 ; Shift bit into bottom of CX
- jnc Flip61Loop ; Loop if more flips (six in total).
- popf ; Restore interrupt flag
- test cl,cl ; Was port read/write? Zero if so.
- jnz MachineAT ; If it's an AT, continue
-
- mov dx,OFFSET NotATMsg
- mov ah,9
- int 21h
- mov al,1 ; Errorlevel
- jmp SHORT Terminate
-
- MachineAT: mov dx,OFFSET ExplanationMsg
- mov ah,9
- int 21h
-
- in al,61h ; Get Port B
- and al,11111101b ; Turn off speaker enable
- or al,00000001b ; Turn on Timer 2 Gate
- out 61h,al
-
- mov al,0B6h ; Set CTC channel 2 for mode 3,
- out 43h,al ; divisor of 47727, giving 20ms
- jmp short $+2 ; high, 20ms low
- mov al,LOW CTC2Divisor
- out 42h,al
- jmp short $+2
- mov al,HIGH CTC2Divisor
- out 42h,al
- jmp short $+2
-
- MainLoop: mov cl,3 ; Set up shift count for later
- mov bx,CTC2Toggles ; Number of channel 2 transitions
- xor dx,dx ; Counter for refreshes
-
- cli ; Lock out interrupts during sample
-
- Loop1: in al,61h ; Read Port B
- test al,00100000b ; Test CTC channel 2 readback
- jz Loop1 ; Wait until high
- Loop2: in al,61h ; Read Port B
- test al,00100000b ; Test CTC channel 2 readback
- jnz Loop2 ; Wait until low
-
- mov ah,al ; Keep old value in AH
-
- Loop3: in al,61h ; Read Port B
- xor al,ah ; Find different bits
- test al,00110000b ; Either bit changed?
- jz Loop3 ; If not, loop
- xor ah,al ; Keep new value
- shl al,cl ; Bit 5 into carry
- sbb bx,0 ; Decrement BX if T2 output changed
- jz Done ; If waited the full sample time
- shl al,1 ; Bit 4 into carry
- adc dx,0 ; Increment DX if refresh occurred
- jmp SHORT Loop3 ; Loop
-
- Done: sti ; Interrupts back on
- mov ax,dx ; Get refresh counter
- call Mach16_DecASC ; Convert to decimal and display
- mov dx,OFFSET NewlineMsg ; CR/LF message
- mov ah,9
- int 21h ; Display it
- mov ah,1 ; Test for keypress pending
- int 16h
- jz MainLoop ; If no key pending
- xor ah,ah ; Zero
- int 16h ; Clear out the key
- xor al,al ; Errorlevel 0
- Terminate: mov ah,4Ch ; Terminate with errorlevel
- int 21h ; Call DOS
- int 20h ; In case DOS-1 (!)
- Main2 ENDP
-
- Mach16_DecASC PROC near
- ; Func: Convert machine 16-bit unsigned value
- ; to ASCII decimal representation and
- ; output via DOS function 2
- ; In: AX = Value to output
- ; Out: None
- ; Lost: AX BX CX DX
- xor cx,cx ; Zero digit counter
- Mach16_DecASC1: xor dx,dx ; Clear high word of value in DX|AX
- mov bx,10 ; Base
- div bx ; Divide by 10
- add dl,"0" ; DL is remainder, convert to ASCII
- push dx ; Store on stack
- inc cx ; Increment char counter
- test ax,ax ; Any more digits left?
- jnz Mach16_DecASC1 ; If so, loop
- Mach16_DecASC2: pop dx ; Get char back
- mov ah,2 ; Print char
- int 21h ; Call DOS
- loop Mach16_DecASC2 ; Loop for all chars
- ret ; Done
- Mach16_DecASC ENDP
-
- ComFile ENDS
- END Main
- -------------------------------- snip snip snip --------------------------------
-
- ## 7.37.2 SAMPLE CODE: DELAY(MILLISECONDS) FUNCTION USING REFRESH DETECT
-
- This function uses the Refresh Detect signal to provide a delay(milliseconds)
- function. This function does not check that the refresh channel is operating
- with the correct divisor. It also does not check that it is running on an AT
- or later machine with the required Port B hardware. If required, these checks
- should be done at the start of the program that will use this function.
-
- -------------------------------- snip snip snip --------------------------------
-
- Params = 4 ; USE 6 FOR FAR CODE MODELS!
-
- _delay PROC near
- push bp ; Preserve BP
- mov bp,sp ; Address stacked parameters
- mov cx,[bp+Params] ; Get loword of number of milliseconds
- mov dx,[bp+Params+2] ; Get hiword
- mov bx,61714 ; Initialise negative count register
- in al,61h ; Read Port B initially
- mov ah,al ; To AH
- jmp SHORT DelayDecr ; Decrement count and loop if nonzero
- DelayLoop: in al,61h ; Read Port B
- xor al,ah ; Get different bits
- test al,00100000b ; Did Refresh Detect toggle?
- jz DelayLoop ; If not, keep waiting
- xor ah,00100000b ; Toggle last known state flag
- sub bx,931 ; Approximating the number of Refresh
- jnb DelayLoop ; of Refresh Detect toggles per
- add bx,61714 ; millisecond as 61714 / 931
- DelayDecr: sub cx,1 ; One millisecond has elapsed
- sbb dx,0 ; Borrow into hiword
- jnb DelayLoop ; If more milliseconds remaining
- pop bp ; Restore BP from caller
- ret
- _delay ENDP
-
- -------------------------------- snip snip snip --------------------------------
-
- The declaration for the above function is:
-
- void delay(unsigned long milliseconds)
-
- The actual number of Refresh Detect toggles per millisecond is 14318.18/216, or
- about 66.3. The above function approximates this ratio to be 61714/931, which
- contributes an error of 0.085767 ppm, less than 1/100th typical crystal error.
- The longest delay that can be generated (milliseconds = 0xFFFFFFFF) is 49 days,
- 17 hours, 2 minutes, and 47.295 seconds. For this duration, the error
- contributed by the approximation is about 0.368 seconds.
-
- The delay(milliseconds) function may be called with interrupts enabled or with
- interrupts disabled. It does not modify the state of the interrupt flag during
- its execution.
-
- If it runs with interrupts enabled, the actual length of the delay will normally
- be longer than specified, due to gaps in processing caused by the timer tick
- interrupt and any other active interrupts (keyboard interrupt, serial port
- interrupt, network card interrupt, etc).
-
- If it runs with interrupts locked out, it will give an accurate delay, but it
- may disrupt the normal operation of the machine by preventing interrupts from
- being processed for an excessive length of time - see sections »» 6.15 to
- »» 6.19 for more information on this problem.
-
- The uncertainty is one refresh period, or about 15.0857 microseconds. The
- overhead is a few microseconds on a fast machine, longer on a slow machine.
-
- ## 8 SPEEDING UP THE TIMER TICK
-
- Note: This section makes many references to earlier sections. I recommend that
- if you are not familiar with the normal operation of the CTC, the timer tick
- interrupt, general interrupt considerations, and interrupt chaining, you should
- first read the related sections and any other sections that seem relevant.
-
- Increasing the timer tick rate involves the following steps:
-
- ■ Intercept int 8, redirecting it to your new int 8 handler
- ■ Intercept and handle the Ctrl-C and Critical Error interrupts so that
- the int 8 vector can be restored if the program is terminated due to
- a Ctrl-C or a critical error
- ■ Reprogram the CTC channel zero divisor for the new interrupt rate
- ■ Maintain a counter within your int 8 handler to schedule chaining to
- the original int 8 handler
- ■ Restore int 8 and restore the normal divisor upon termination
-
- See section »» 6.3 for details of how to intercept an interrupt. See section
- »» 6.31 for information on chaining to the old interrupt handler, and section
- »» 5 and subsections for information about the Ctrl-C and Critical Error
- interrupts and how to handle them. See section »» 7.10 for how to program the
- divisor. See section »» 6.31 for details on how to chain to the original int
- 8 handler, and sections »» 6.28 and »» 6.28.1 for information on ending
- interrupt routines when they are not chained. The comments in section »» 6.15
- and section »» 6.16 and subsections regarding interrupt jitter also apply when
- the timer tick is operated at a faster rate, because the maximum period for
- which interrupts can be locked out without loss of a timer interrupt becomes
- shorter as the timer interrupt rate is increased. Changing the divisor and/or
- operating mode of CTC channel 0 may also break the BIOS's joystick reading
- functions (see section »» 10.4.2). Also see section »» 8.4.
-
- The technique of speeding up the timer tick should not be used in TSRs because
- foreground programs are at liberty to use and reprogram the CTC chip for their
- own purposes.
-
- ## 8.1 THE FAST TICK INT 8 HANDLER
-
- Having reprogrammed the timer tick interrupt to operate at a higher speed, you
- must ensure that other software that uses int 8 (see section »» 6.1) is called
- at the correct rate, i.e. 18.2065 times per second. This is achieved with a
- counter variable, which duplicates the behaviour of CTC channel zero when it is
- operating with the normal divisor of 65536 (see section »» 7.4 and subsections).
-
- This operates by maintaining a 16-bit variable which accumulates CTC clock
- periods and will overflow after 65536 CTC clocks, indicating that another
- 54.9254 ms have elapsed. This variable is maintained by the new int 8
- handler. Every time int 8 is signalled, the new channel zero divisor value
- (which represents the number of CTC clocks since the last int 8) is added
- into this variable, and if the variable carries (i.e. exceeds 65535 and wraps
- around), the old int 8 handler is called (i.e. is scheduled). If the variable
- does not wrap around, then the old int 8 handler is not called.
-
- For example, assume CTC channel zero is operating with a divisor of 1234
- (decimal). This will give a fast tick rate of 1193181.66666... / 1234, or
- about 967 ticks per second. Each time the new int 8 handler is triggered,
- 1234 CTC clocks have elapsed (since the last time it was triggered), so we
- add 1234 into the scheduler variable, representing the number of CTC clocks
- that have just elapsed.
-
- When the variable wraps around (i.e. the processor's carry flag is set after
- the addition), another 65536 CTC clocks have elapsed, so it is time to chain
- to the original int 8 handler, which expects to be called every 65536 CTC
- clocks (the "slow tick" rate, if you like). Thus the variable mimics the CTC
- channel zero counting register when programmed for a divisor of 65536.
-
- Of course the slow ticks (calls to the old int 8 handler) will not be perfectly
- evenly spaced, but in almost all applications, variations are acceptable as
- long as they are not cumulative, and they will not be cumulative (unless fast
- ticks are missed, see section »» 6.16 and subsections), and if the new tick
- rate is high (like the example above) they will be fairly even. The worst slow
- tick jitter will occur with divisors near to 32768, where calls to the slow
- tick handler could be up to almost 32768 CTC clocks early or late. Even this
- will not be a problem under DOS, in most circumstances.
-
- ## 8.2 THE INTERFACE WITH THE MAINLINE
-
- Generally the fast tick handler will have some sort of interface with the
- mainline (the foreground process) of your program. Typically this will be
- implemented via shared variables. These variables transfer control information
- from the mainline to the interrupt routine (as in the Morse code player example
- program described later), or may transfer status or time information from the
- interrupt routine to the mainline (as in the one millisecond timer program also
- described later), or a combination of both.
-
- The shared variables can be put in either the code segment or the data segment.
- For a COM file (tiny model) the segments are the same, and this makes things
- quite convenient. See section »» 6.32.1 for more information.
-
- ## 8.3 WRITING A FAST TICK HANDLER
-
- Fast tick handlers are often written in assembly language as it is more
- convenient and more efficient, though the latter advantage is largely mooted
- by the speed of modern processors.
-
- Refer to section »» 6.32 and subsections for a discussion of guidelines that
- must be applied when writing an interrupt handler in assembly language. The
- fast tick handler must follow these guidelines.
-
- After the housekeeping instructions, the fast tick interrupt handler should
- perform the function that it is required for, then before it exits, it should
- handle chaining to the slow tick interrupt handler. This involves adding the
- divisor value into the scheduler variable and deciding whether to chain to the
- slow tick handler or not.
-
- If it decides to chain, it can use the JMP chaining method (see section »» 6.31
- for details). If it does not chain, it must send an EOI signal to the PIC (see
- section »» 6.28) and return with an IRET. To support Microchannel machines, it
- may be necessary to acknowledge the int 8 - see section »» 6.28.1.
-
- Here is an example fast tick interrupt handler written in assembler for tiny
- model (i.e. a COM file). It increments the FastTickCount variable on each
- fast tick. This variable is for use by the mainline.
-
- -------------------------------- snip snip snip --------------------------------
- FastTickRate EQU 1234 ; This is the new fast tick rate
-
- FastTickCount DW 0 ; Counter variable for use by mainline
- SlowTickSched DW 0 ; Schedule control var for slow tick
-
- ASSUME cs:_TEXT,ds:nothing,es:nothing,ss:nothing
-
- NewInt8Handler PROC far
- pushf ; Keep flags
- push ax ; Keep AX
-
- ; Push any other registers you will modify
-
- inc FastTickCount ; This is the 'action' in this example
-
- ; Pop any other registers you pushed, in
- ; reverse order, but do not pop AX or Flags
-
- add SlowTickSched,FastTickRate ; Add ticks into variable
- jnc NoSchedule ; If it didn't carry
-
- ; Another 54.9254 ms has elapsed - chain to the slow tick handler
-
- pop ax ; Restore AX
- popf ; Restore flags
- DB 0EAh ; JMP xxxx:yyyy
- Old08Ofs DW 0 ; Vector to original handler - Offset
- Old08Seg DW 0 ; Segment
-
- ; Not time to chain yet - send EOI and return. Note - may not support
- ; Microchannel machines which may require a hardware int 8 acknowledge
- ; signal to be issued.
-
- NoSchedule: mov al,20h ; EOI code for PIC
- out 20h,al ; Send
- pop ax ; Restore AX
- popf ; Restore flags
- iret
- NewInt8Handler ENDP
- -------------------------------- snip snip snip --------------------------------
-
- Of course to use this interrupt handler, you must have first intercepted int 8
- and reprogrammed CTC channel zero with a divisor of 1234. Also for safety you
- must have intercepted the DOS Ctrl-C and Critical Error interrupts so that the
- original channel zero divisor, and original int 8 handler address, can be
- restored if the program terminates for either of these reasons.
-
- ## 8.4 COMMENTS ON FAST TIMER TICK INTERRUPTS
-
- {JAM} makes some good comments about this (slightly paraphrased):
-
- "Speeding up the timer tick interrupt presents two problems. The first is the
- increased load on the CPU, and the second is that any routine that disables
- interrupts for over twice the fast tick interrupt period will cause a missed
- interrupt. Masking for less then the interrupt period will cause interrupt
- delivery jitter and maybe the loss of a fast tick interrupt.
-
- "In the days of 8 MHz ATs, the former problem was the dominant one; now with
- faster computers and more complicated operating systems and TSR programs, the
- more subtle second problem dominates."
-
- Klaus Hartnegg (klaus@mailserv.brain.uni-freiburg.de) tried using a fast timer
- interrupt at 4, 6, and 8 kHz, and reports "serious problems with interrupts
- generated by network and keyboard (especially bad with DOS's KEYB.COM driver,
- a lot better with a freeware replacement). I have come to the conclusion that
- it's probably not possible to rely on such a high frequency timer interrupt.
- There are too many periods of time with disabled interrupts that cause lost
- interrupts."
-
- ## 8.5 SAMPLE PROGRAM: MORSE PLAYER USING FAST TIMER TICK
-
- The following program demonstrates the techniques involved in operating the
- timer tick at a higher speed. The tickdiv variable contains the new divisor
- that is programmed into CTC channel zero. The int8sched variable controls
- chaining to the original handler. It duplicates the normal action of CTC
- channel zero when programmed with a divisor of 65536, as described in section
- »» 8.3.
-
- A single queue interface is used to transfer control information from the
- mainline to the timer tick handler. The int 8 routine has full control of the
- speaker hardware, and generates beeps using CTC channel two in response to
- control words sent from the mainline via the queue.
-
- Most of the code is fairly self-explanatory. The fast tick interrupt is
- chosen according to the speed selected via the command line parameter, which
- may be any number from 1 to 99. The speed doubles for each decade. There are
- ten divisors, contained in the tickdivs[] array. Within each decade of speed
- numbers, the tickdivs[] values give a smoothly increasing speed, then from one
- decade to the next, the number of interrupts per 'dit' or 'dah' goes up in
- powers of two, resulting in a smooth speed scale, with each decade giving a
- 2:1 increase in playback speed. For testing, a speed of 50 is reasonable.
-
- By the way, when speaking Morse code, speak a '.' as 'dit' and '-' as 'dah',
- and join a dit to any following dit or dah in the same letter code. So, for
- example, the Morse code for the letter C ("-.-.") is spoken "dah-di-dah-dit".
-
- A proper Morse code player would be much more powerful, but this program is
- coming dangerously close to being useful. I will be more careful in future :-)
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #13
- Demonstrates fast timer tick
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save and assemble the critical error module CRIT_ERR
- Save this sample code to SAMPLE13.C
- Compile this module with:
- bcc -c -I<inc_path> -ms sample13.c
- Link the modules with:
- tlink /c /x <c0_path>\c0s.obj sample13.obj crit_err.obj,
- sample13, nul, <lib_path>\cs
- Where inc_path is the path to your C header files, c0_path is the path to your
- startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <dos.h> /* Needed for MK_FP() */
- #include <io.h> /* Needed for _write() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDERR 2 /* DOS handle for standard error */
-
- #define MORSEBUFSIZE 128 /* Number of entries in morse data buffer */
-
- #define BEEP_DIVISOR 2000 /* Freq = 1193182 / BEEP_DIVISOR */
-
- #define DIT_LENGTH 1 /* Length of a dit */
- #define DAH_LENGTH 3 /* Length of a dah */
- #define DIT_SPACING 1 /* Spacing between dits/dahs within a letter */
- #define LETTER_SPACING 3 /* Spacing between letter codes */
- #define WORD_SPACING 6 /* Spacing between words */
- #define STOP_SPACING 10 /* Spacing after a full stop (period) '.' */
-
- #define ONOFF 0x8000 /* Top bit controls tone on or off */
-
- static unsigned int tickdivs[10] = {
- 16384, 15287, 14263, 13308, 12417,
- 11585, 10809, 10086, 9410, 8780
- };
-
- static unsigned int morsebuf[MORSEBUFSIZE]; /* Communication between
- mainline and int 8 stuff */
-
- /* Characters for morsecode array:
-
- "" Ignore this code completely
- "w" Word space - enforce a word spacing at this point
- "s" Stop space - enforce a full stop spacing at this point
- ".--." Actual code to send, using letter spacing at end */
-
- static unsigned char morsecode[128][7] = {
- "", "", "", "", "", "", "", "w", /* 0 to 7 */
- "", "w", "w", "", "w", "w", "", "", /* 8 to 15 */
- "", "", "", "", "", "", "", "", /* 16 to 23 */
- "", "", "", "", "", "", "", "", /* 24 to 31 */
- "w", "", "", "", "", "", "", "", /* ' ' to ''' */
- "", "", "", "", "", "", "s", "", /* '(' to '/' */
- "-----", ".----", "..---", "...--", "....-", /* 0 to 4 */
- ".....", "-....", "--...", "---..", "----.", /* 5 to 9 */
- "w", "", "", "", "", "", "", /* ':' to '@' */
- ".-", "-...", "-.-.", "-..", ".", /* 'A' to 'E' */
- "..-.", "--.", "....", "..", ".---", /* 'F' to 'J' */
- "-.-", ".-..", "--", "-.", "---", /* 'K' to 'O' */
- ".--.", "--.-", ".-.", "...", "-", /* 'P' to 'T' */
- "..-", "...-", ".--", "-..-", "-.--", "--..", /* 'U' to 'Z' */
- "", "", "", "", "", "", /* '[' to '`' */
- ".-", "-...", "-.-.", "-..", ".", /* 'a' to 'e' */
- "..-.", "--.", "....", "..", ".---", /* 'f' to 'j' */
- "-.-", ".-..", "--", "-.", "---", /* 'k' to 'o' */
- ".--.", "--.-", ".-.", "...", "-", /* 'p' to 't' */
- "..-", "...-", ".--", "-..-", "-.--", "--..", /* 'u' to 'z' */
- "", "", "", "", "" /* '{' to Del */
- };
-
- static unsigned int timescaler; /* Time range scaler */
-
- static unsigned int inptr;
- static volatile unsigned int outptr; /* Offsets into morsebuf */
-
- static unsigned int tickdiv; /* Actual chosen tick divisor */
-
- void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
- unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
-
- typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
-
- intfuncp old_int8 = (intfuncp)0xFFFFFFFFL;
-
- /* Communication between the mainline and the int 8 handler is via the morsebuf
- array, which is used as a queue. Each entry in morsebuf is a 16-bit unsigned
- int. The top bit determines whether the beeping sound should be turned on
- (if set) or off (if clear), and the remaining bits determine how many fast
- ticks the int 8 routine will wait after actioning the top bit, before it
- fetches the next word from morsebuf. Access to morsebuf is controlled by
- the in and out pointers, which are actually offsets, not pointers. When
- these are equal, the queue is empty. */
-
- void interrupt int8_handler(void) {
- static unsigned int int8counter = 0;
- static unsigned int int8sched = 0;
- if (int8counter)
- --int8counter;
- if ((int8counter == 0) && (outptr != inptr)) { /* Data there */
- int8counter = morsebuf[outptr];
- ++outptr;
- if (outptr >= MORSEBUFSIZE) /* Bump out ptr */
- outptr = 0;
- if (int8counter & ONOFF) { /* Turn sound on */
- outportb(0x43, 0xB6); /* Ch 2, mode 3 */
- outportb(0x42, (BEEP_DIVISOR & 0xFF)); /* Lobyte */
- outportb(0x42, (BEEP_DIVISOR >> 8)); /* Hibyte */
- outportb(0x61, inportb(0x61) | 0x03); /* Speaker on */
- }
- else { /* Turn sound off */
- outportb(0x61, inportb(0x61) & 0xFC); /* Speaker off */
- }
- int8counter &= (~ ONOFF); /* Remove on/off bit */
- }
- int8sched += tickdiv;
- if (int8sched < tickdiv) { /* If carried */
- (old_int8)(); /* Chain to BIOS */
- }
- else
- /** note - may not support Microchannel machines */
- outportb(0x20, 0x20); /* Send EOI if not chaining */
- return; /* From interrupt */
- }
-
- void restore_normal(void) {
- asm pushf;
- asm cli;
- outportb(0x43, 0x36);
- outportb(0x40, 0);
- outportb(0x40, 0); /* Restore normal divisor */
- outportb(0x61, inportb(0x61) & 0xFC); /* Speaker off */
- asm popf;
- return;
- }
-
- void abort_cleanup(int dos_is_safe) {
- if (dos_is_safe) {
- if (old_int8 != (intfuncp)0xFFFFFFFFL) {
- setvect(0x08, old_int8);
- old_int8 = (intfuncp)0xFFFFFFFFL;
- }
- }
- else {
- if (old_int8 != (intfuncp)0xFFFFFFFFL) {
- *((intfuncp far *)MK_FP(0, 0x08 << 2)) = old_int8;
- old_int8 = (intfuncp)0xFFFFFFFFL;
- }
- }
- restore_normal();
- return;
- }
-
- void interrupt ctrl_c_handler(void) {
- static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
- if (is_at_crit_prompt())
- abort_cleanup(FALSE);
- else {
- abort_cleanup(TRUE);
- _write(STDERR, &message, sizeof(message));
- }
- exit(255);
- }
-
- void poll_exit(void) {
- if (bioskey(1)) {
- if ((bioskey(0) & 0xFF) == 27) {
- setvect(0x08, old_int8);
- old_int8 = (intfuncp)0xFFFFFFFFL;
- restore_normal();
- exit(0);
- }
- }
- return;
- }
-
- void putmorse(unsigned int codeval) {
- unsigned int tempptr;
- poll_exit();
- tempptr = inptr + 1;
- if (tempptr >= MORSEBUFSIZE)
- tempptr = 0;
- while (outptr == tempptr)
- poll_exit(); /* Wait for space in the queue */
- codeval = (((codeval & (~ONOFF)) << timescaler) | (codeval & ONOFF));
- morsebuf[inptr] = codeval;
- inptr = tempptr;
- return;
- }
-
- void playmorse(char * str) {
- char ch;
- char * cp;
- unsigned int was_word;
- was_word = FALSE;
- cp = str;
- while ((ch = *cp++) != '\0') {
- switch (ch) {
- case 'w' :
- putmorse(WORD_SPACING);
- break;
- case 's' :
- putmorse(STOP_SPACING);
- break;
- case '.' :
- putmorse(DIT_LENGTH | ONOFF);
- putmorse(DIT_SPACING);
- was_word = TRUE;
- break;
- case '-' :
- case '_' :
- putmorse(DAH_LENGTH | ONOFF);
- putmorse(DIT_SPACING);
- was_word = TRUE;
- break;
- }
- }
- if (was_word)
- putmorse(LETTER_SPACING);
- return;
- }
-
- void main(int argc, char * argv[]) {
- unsigned int speedrange;
- int ch;
- FILE * infile;
-
- printf("Sample program #13 - Morse code player demonstrating fast timer tick\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
-
- if ((argc < 3) || (strlen(argv[1]) != 2)) {
- printf("Usage: SAMPLE13 speed filename\n\n");
- printf("\tspeed = 10 to 99, speed doubles each decade\n");
- printf("\tfilename = name of file to be played\n");
- exit(1);
- }
-
- timescaler = 8 - (argv[1][0] - '1'); /* Shift count for timings */
- speedrange = argv[1][1] - '0'; /* Fine speed, 0-9 */
- if ((timescaler > 8) || (speedrange > 9)) {
- printf("Speed out of range\n");
- exit(2);
- }
-
- tickdiv = tickdivs[9 - speedrange];
-
- infile = fopen(argv[2], "r");
- if (infile == NULL) {
- printf("Could not open input file '%s'\n", argv[2]);
- exit(4);
- }
-
- printf("Press <Esc> to exit\n");
-
- crit_err_intercept(); /* Trap critical errors */
- setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
- old_int8 = (intfuncp)getvect(0x08);
- setvect(0x08, int8_handler);
-
- asm cli;
- outportb(0x43, 0x36);
- outportb(0x40, tickdiv & 0xFF);
- outportb(0x40, tickdiv >> 8);
- asm sti;
-
- while ((ch = fgetc(infile)) != EOF)
- if (ch < 0x80)
- playmorse(morsecode[ch]);
-
- putmorse(1); /* Make sure the speaker is off */
- while (inptr != outptr)
- ; /* Wait for buffer to empty */
-
- setvect(0x08, old_int8);
- old_int8 = (intfuncp)0xFFFFFFFFL;
- restore_normal();
-
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 8.6 DYNAMIC FAST TICK PERIODS
-
- In the Morse code player sample program, once the new fast tick rate has been
- chosen and programmed, it is not modified until the program terminates. The
- interrupt keeps occurring regularly at the fast tick rate. However, it is
- possible to dynamically change the fast tick rate on a per-interrupt basis.
-
- There are several reasons why you might want to do this - I can think of
- four applications, there may be more:
-
- ■ You might want to create a signal with an uneven or completely
- arbitrary duty cycle, such as 5 ms high, 40 ms low (this example
- could also be done using a constant fast tick at 5 ms intervals
- and counting eight interrupts to get the 40 ms delay),
-
- ■ You might be using the timer interrupt to schedule things which
- happen at irregular intervals, with some long gaps, some short,
-
- ■ You might want your background interrupt routine to be able to
- adjust its speed according to user actions, such as keypresses
- which control the program 'speed',
-
- ■ You might want an exact number of interrupts per second, which
- is not possible with a fixed divisor - see section »» 8.7 for
- a sample program that does this.
-
- All of these requirements can be handled in the same way. The technique
- involves the interrupt routine adjusting the value in the Reload register
- according to its requirements, to adjust the period between interrupts in a
- dynamic fashion.
-
- When the fast timer tick interrupt handler reprograms the Reload register, the
- new Reload register value does not affect the current countdown in progress,
- i.e. the length of time until the next interrupt, it affects the length of
- time between the next interrupt and the interrupt after that. In other words,
- you could say there is a one interrupt delay before the new value takes effect.
-
- ## 8.7 SAMPLE PROGRAM: DYNAMIC FAST TICK INTERRUPT HANDLER
-
- This sample program gives a fast tick rate of exactly 1000 fast ticks per
- second, using an effective divisor of 1193.18166666....
-
- This cannot be achieved with a static divisor - the closest static divisors
- of 1193 and 1194 produce 1000.152277 and 999.3146287 interrupts per second
- respectively. To get exactly 1000 fast ticks per second, the divisor must
- be changed dynamically to give an effective divisor of 1193.181666... by
- cycling through the appropriate sequence of 1193 and 1194 divisors. Over a
- short period of time, the tick rate will rapidly approach exactly 1000 ticks
- per second (ignoring the error due to crystal inaccuracies, etc).
-
- The sequence of divisors is determined as follows. For 1000 ticks per second
- the divisor is 1193.18166666... which is 1193 plus 9/50 (0.18) plus 1/600
- (0.0016666...), which is also 1193 plus 1/5 minus 1/50, plus 1/600.
-
- Count every fifth interrupt. On four out of every five interrupts, use a
- divisor of 1193, but on every fifth interrupt, when the divide-by-five counter
- carries, prepare to use 1194, and count a divide by 10 counter (which is really
- dividing by 50). If the counter _doesn't_ carry, use 1194. These two counters
- in combination add the 9/50. If the divide by 10 counter carries, prepare to
- use 1193, and count down a divide by 12 counter, which is actually counting
- 1/600ths; if it carries, use 1194.
-
- A similar approach could be used to get 200 fast tick interrupts per second
- (i.e. a 5ms fast tick interval). The divisor is 5965.90833333333, which is
- 5965 plus 9/10 plus 1/120, so you would use 5966 for 9 of every 10 cycles and
- use 5965 on the tenth, except if it is the twelfth tenth cycle in which case
- use 5966.
-
- Mode two must be used for this technique. See the description of behaviour
- with odd divisors in section »» 7.8.5 for the reasons.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #14
- Demonstrates dynamic timer tick rates
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save and assemble the critical error module CRIT_ERR
- Save this sample code to SAMPLE14.C
- Compile this module with:
- bcc -c -I<inc_path> -ms sample14.c
- Link the modules with:
- tlink /c /x <c0_path>\c0s.obj sample14.obj crit_err.obj,
- sample14, nul, <lib_path>\cs
- Where inc_path is the path to your C header files, c0_path is the path to your
- startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <dos.h> /* Needed for MK_FP() */
- #include <io.h> /* Needed for _write() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDERR 2 /* DOS handle for standard error */
-
- #define BASETICK 1193
-
- void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
- unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
-
- typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
-
- intfuncp old_int8 = (intfuncp)0xFFFFFFFFL;
-
- static volatile unsigned long milliseconds = 0; /* Milliseconds counter */
-
- /* The interrupt handler is responsible for updating the tick divisor to give
- exactly 1000 ticks per second. It also increments a 32-bit counter which
- is used by the mainline. */
-
- void interrupt int8_handler(void) {
- static unsigned int div_5 = 2;
- static unsigned int div_5_10 = 5;
- static unsigned int div_5_10_12 = 6;
- static unsigned int int8sched = 0;
- static unsigned int fastdiv = 0;
- asm {
- mov ax,1193 /* Prepare to use 1193 */
- dec [div_5] /* Count down divide by 5 */
- jns GotNewDiv /* If not reached one fifth yet */
- mov [div_5],4 /* Reset dividing register */
- inc ax /* Prepare to use 1194 */
- dec [div_5_10] /* Count down nested divide by 10 */
- jns GotNewDiv /* If not reached 1/10 of 1/5 yet */
- mov [div_5_10],9 /* Reset dividing register */
- dec ax /* Prepare to use 1193 */
- dec [div_5_10_12] /* Count down nested divide by 12 */
- jns GotNewDiv /* If not reached 1/12 of 1/10 of 1/5 */
- inc ax /* The 1/600th! */
- mov [div_5_10_12],11 /* Reset dividing register */
- }
- GotNewDiv:
- asm {
- cmp ax,[fastdiv] /* Got divisor in AX - did it change? */
- je SameDiv /* If not, don't reprogram CTC 0 */
- mov [fastdiv],ax /* Store new value */
- out 40h,al /* Write new lobyte */
- mov al,ah /* Get hibyte */
- out 40h,al /* Write new hibyte */
- }
- SameDiv: /* End of inline assembly */
- ++milliseconds; /* Increment millisecond count */
- int8sched += fastdiv;
- if (int8sched < fastdiv) { /* If carried */
- (old_int8)(); /* Chain to BIOS */
- }
- else
- /** note - may not support Microchannel machines */
- outportb(0x20, 0x20); /* Send EOI if not chaining */
- return; /* From interrupt */
- }
-
- void restore_normal(void) {
- asm pushf;
- asm cli;
- outportb(0x43, 0x36);
- outportb(0x40, 0);
- outportb(0x40, 0); /* Restore normal divisor */
- asm popf;
- return;
- }
-
- void abort_cleanup(int dos_is_safe) {
- if (dos_is_safe) {
- if (old_int8 != (intfuncp)0xFFFFFFFFL) {
- setvect(0x08, old_int8);
- old_int8 = (intfuncp)0xFFFFFFFFL;
- }
- }
- else {
- if (old_int8 != (intfuncp)0xFFFFFFFFL) {
- *((intfuncp far *)MK_FP(0, 0x08 << 2)) = old_int8;
- old_int8 = (intfuncp)0xFFFFFFFFL;
- }
- }
- restore_normal();
- return;
- }
-
- void interrupt ctrl_c_handler(void) {
- static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
- if (is_at_crit_prompt())
- abort_cleanup(FALSE);
- else {
- abort_cleanup(TRUE);
- _write(STDERR, &message, sizeof(message));
- }
- exit(255);
- }
-
- void poll_exit(void) {
- if (bioskey(1)) {
- if ((bioskey(0) & 0xFF) == 27) {
- setvect(0x08, old_int8);
- old_int8 = (intfuncp)0xFFFFFFFFL;
- restore_normal();
- exit(0);
- }
- }
- return;
- }
-
- void main(void) {
- unsigned long ms;
-
- printf("Sample program #14 - Millisecond timer demonstrating dynamic timer tick\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Press <Esc> to exit\n\n");
-
- crit_err_intercept(); /* Trap critical errors */
- setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
- old_int8 = (intfuncp)getvect(0x08);
- setvect(0x08, int8_handler);
-
- asm cli;
- outportb(0x43, 0x34); /* Must use mode two! */
- outportb(0x40, BASETICK & 0xFF);
- outportb(0x40, BASETICK >> 8);
- asm sti;
-
- while (1) {
- asm cli;
- ms = milliseconds;
- asm sti;
- printf("%010ld ms\r", ms);
- poll_exit();
- }
- }
- -------------------------------- snip snip snip --------------------------------
-
- Note the order in which things are restored to normal in poll_exit(). The call
- to restore_normal() to set the tick rate back to normal, must appear after the
- fast tick handler has been disconnected. If the fast tick handler was still
- connected after the tick rate was set to normal, it could reprogram the CTC
- again, and the program would terminate with the tick running with a divisor of
- 1193 or 1194 (at roughly 1ms intervals)!
-
- This program could be used to measure the execution time of another program
- with 1ms resolution, provided that the other program did not use CTC channel
- zero itself, and provided that the other program did not lock interrupts out
- for more than 1ms at a time.
-
- ## 9 READING AN ABSOLUTE TIMESTAMP
-
- It is possible to read an absolute timestamp at any moment in time. This
- timestamp is comprised of the value read from the Counter Latch register in
- channel 0 of the CTC (see sections »» 7.14 and »» 7.15), which is 16 bits
- wide, and the BIOS Tick Count variable (see section »» 4) which is 32 bits
- wide, though only the bottom 21 bits are used.
-
- Mode two is easiest to use for this purpose. BIOSes traditionally set up CTC
- channel zero to run in mode three, but recent BIOSes seem to be using mode two.
- See section »» 7.4.2 for details.
-
- In order to be able to read an absolute timestamp, your program must first
- ensure that CTC channel zero is operating in mode two with a divisor of 65536
- and that the lobyte/hibyte flag is in sync. This is most easily ensured by
- simply setting the mode and divisor in the initialisation section of your
- program. See section »» 7.10 and section »» 7.12 for details.
-
- Reading the count in progress is described in sections »» 7.15, »» 7.15.1, and
- »» 7.16. Reading the BIOS tick count variable is described in sections »» 4.5
- and »» 4.6.
-
- To ensure that a correct value is read, it is necessary to read the BIOS tick
- count first, then read the count in progress in the CTC, then enable interrupts,
- then re-read the BIOS tick count, then work out whether the first or second BIOS
- tick count value is appropriate (if they are different). This is demonstrated
- in the sample program in section »» 9.1.
-
- ## 9.1 SAMPLE PROGRAM: ABSOLUTE TIME REFERENCE (TIMESTAMP) IN MODE TWO
-
- This program demonstrates the initialisation required to set the timer to run
- in mode two, and a function that will return an absolute timestamp, in units
- of 0.8381 microseconds since midnight on the current day. Initially it will
- display the timestamp every time a key is pressed. Once the <Esc> key is
- pressed, it goes into continuous timestamp checking mode, where it continuously
- requests and displays the absolute timestamp, and also checks that the
- timestamp never goes backwards. If the timestamp goes backwards, it displays
- the two timestamp values before the error, and the first timestamp after the
- negative increment. This will normally occur only at midnight.
- Pressing <Esc> again will terminate the program.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #15
- Demonstrates absolute timestamping in mode two
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE15.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample15.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
-
- */
-
- #pragma inline; /* Required for asm pushf, popf, cli, and sti */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- typedef struct {
- unsigned int part;
- unsigned long ticks;
- } timestamp;
-
- #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
-
- void set_mode2(void) {
- auto unsigned int tick_loword;
- tick_loword = * BIOS_TICK_COUNT_P;
- while ((unsigned int) * BIOS_TICK_COUNT_P == tick_loword)
- ;
- asm pushf;
- asm cli;
- outportb(0x43, 0x34); /* Channel 0, mode 2 */
- outportb(0x40, 0x00); /* Loword of divisor */
- outportb(0x40, 0x00); /* Hiword of divisor */
- asm popf;
- return;
- }
-
- void get_timestamp(timestamp * tsp) {
- auto unsigned long tickcount1, tickcount2;
- auto unsigned int ctcvalue;
- auto unsigned char ctclow, ctchigh;
- asm pushf;
- asm cli;
- tickcount1 = * BIOS_TICK_COUNT_P;
- outportb(0x43, 0); /* Latch value */
- ctclow = inportb(0x40);
- ctchigh = inportb(0x40); /* Read count in progress */
- asm sti; /* Force interrupt ENABLE */
- ctcvalue = - ((ctchigh << 8) + ctclow);
- tickcount2 = * BIOS_TICK_COUNT_P;
- asm popf;
- if ((tickcount2 != tickcount1) && (ctcvalue & 0x8000))
- tsp->ticks = tickcount1;
- else
- tsp->ticks = tickcount2;
- tsp->part = ctcvalue;
- return;
- }
-
- void main(void) {
- auto timestamp ts, ts1, ts2;
- auto unsigned int sched;
- auto unsigned int ch;
- printf("Sample program #15 - Demonstrates absolute timestamping\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Press any key to get timestamp; press <Esc> for continuous test\n\n");
-
- set_mode2();
-
- do {
- ch = bioskey(0); /* Get a keypress */
- get_timestamp(&ts); /* Get timestamp */
- printf("Absolute timestamp: 0x%04X%04X%04X units of 0.8381 us\n",
- (unsigned int) (ts.ticks >> 16),
- (unsigned int) (ts.ticks & 0xFFFF),
- ts.part);
- } while ((ch & 0xFF) != 27);
-
- printf("\nProgram is now performing continuous timestamp test\n\n");
- printf("Press <Esc> to exit\n\n");
-
- while (1) {
- ts2.ticks = ts1.ticks;
- ts2.part = ts1.part;
- ts1.ticks = ts.ticks;
- ts1.part = ts.part;
- get_timestamp(&ts);
- printf("0x%04X%04X%04X\r",
- (unsigned int) (ts.ticks >> 16),
- (unsigned int) (ts.ticks & 0xFFFF),
- ts.part);
- if ((ts.ticks < ts1.ticks) || ((ts.ticks == ts1.ticks) &&
- (ts.part < ts1.part))) { /* Went backwards? */
- printf("Timestamp went backwards: 0x%04X%04X%04X, 0x%04X%04X%04X, then 0x%04X%04X%04X\n",
- (unsigned int) (ts2.ticks >> 16),
- (unsigned int) (ts2.ticks & 0xFFFF),
- ts2.part,
- (unsigned int) (ts1.ticks >> 16),
- (unsigned int) (ts1.ticks & 0xFFFF),
- ts1.part,
- (unsigned int) (ts.ticks >> 16),
- (unsigned int) (ts.ticks & 0xFFFF),
- ts.part);
- }
- ++sched;
- if (!(sched & 0xFF))
- if (bioskey(1))
- if ((bioskey(0) & 0xFF) == 27)
- break;
- }
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- The interrupt flag is carefully controlled inside the get_timestamp() function.
- Interrupts must remain enabled during normal execution of the program, so that
- the tick interrupt can maintain the BIOS tick count variable which forms part
- of the timestamp value.
-
- The state of the interrupt flag on entry to get_timestamp() is not important,
- but the function will enable interrupts during its operation.
-
- This program can be modified to support mode 3 operation of CTC channel zero
- but this is not necessary as there are no disadvantages to operating the CTC
- in mode two.
-
- {JAM} says that this technique gives an accurate timestamp with a resolution
- of few microseconds. On the computer he used, an Epson 20MH 386/SX, "Reasonable
- clock code is accurate to about 4 microseconds with a minimum read time of about
- 20 microseconds. The clock accuracy does not change much between machines and
- is never under 1 microseconds or over 4".
-
- {JAM} also points out that timer reads take in the region of three to eight
- CTC clock periods and therefore you cannot just wait for a particular time
- value to occur, because you probably will not sample the count at exactly the
- right time. You have to check for _at least_ that length of time elapsed.
-
- Finally, because the absolute timestamp value ranges from 0x000000000000 to
- 0x001800AFFFFF then wraps around to midnight, subtracting two timestsamp
- values will not give a correct indication of elapsed time if the period
- measured crossed a midnight boundary. See section »» 9.3 for details.
-
- ## 9.2 SAMPLE PROGRAM: ABSOLUTE TIMESTAMP IN MODE TWO - ASSEMBLER
-
- This program implements the second section of the sample program from section
- »» 9.1 but is written in assembler and performs direct screen writes. The
- get_timestamp() function is much more carefully optimised. To get an idea of
- how often this program can read the timer, update the number in screen memory,
- and perform several other tasks, set your system time to 23:59:50 and run the
- program, and note how far apart the three reported numbers are. On my
- 486DX2-66, they are mostly between 16 and 32 CTC clocks, and the GetTimestamp
- function takes between 7 and 9 CTC clocks to read its timestamp.
-
- For maximum speed, this program uses the BIOS Ctrl-Break flag at 0000:0471 to
- allow the program to be terminated, so you must press Ctrl-Break to terminate
- the program.
-
- -------------------------------- snip snip snip --------------------------------
- NAME SAMPLE16
-
- ; Sample program #16
- ; Demonstrates absolute timestamping using mode 2, in assembler
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This program assembles into SAMPLE16.COM, a small program which sets CTC
- ; channel 0 to mode 2 and repeatedly reads an absolute timestamp using the
- ; BIOS tick count variable and the count in progress in CTC channel 2, and
- ; displays the 48-bit timestamp (37 bits of which are actually used) as a
- ; 12-digit hex number in the bottom left hand corner of the screen. It also
- ; checks for the timestamp going backwards, and if this occurs, displays a
- ; message giving the two timestamps prior to the timestamp going backwards,
- ; and the timestamp on which the error was detected. If midnight passes,
- ; this message should be displayed, as the timestamp is only an offset into
- ; the current day.
- ;
- ; This program assumes it is running in text mode. It supports colour and
- ; monochrome systems and 43-line and 50-line modes.
- ;
- ; Save this file to SAMPLE16.ASM and assemble with:
- ; masm sample16;
- ; link sample16;
- ; exe2bin sample16.exe sample16.com
- ; or
- ; tasm sample16;
- ; tlink /t sample16;
- ;
-
- ComFile SEGMENT
- ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing
-
- ORG 100h ; Com-type file
-
- Main PROC near
- jmp Main2 ; Skip
- Main ENDP
-
- InitialMsg DB 13,44 DUP(10)
- DB "Sample program #16 - Demonstrates absolute timestamping in mode 2",13,10
- DB "Part of the PC Timing FAQ / Application notes",13,10
- DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10
- DB "Press Ctrl-Break to terminate program",13,10,13,10,"$"
-
- BackwardsMsg DB 13,"Timestamp went backwards: 0x"
- Backwards1 DB "xxxxxxxxxxxx, 0x"
- Backwards2 DB "xxxxxxxxxxxx, then 0x"
- Backwards3 DB "xxxxxxxxxxxx",13,10,13,10,"$"
-
- HexBuffer DB "xxxxxxxxxxxx"
-
- TimeL DW 0 ; Time loword (CTC count)
- TimeM DW 0 ; Time midword (loword of tick count)
- TimeH DW 0 ; Time hiword (hiword of tick count)
- Time1L DW 0 ; Old time loword
- Time1M DW 0 ; Old time midword
- Time1H DW 0 ; Old time rock 'n' roll
- Time2L DW 0 ; Old old time loword
- Time2M DW 0 ; Old old time midword
- Time2H DW 0 ; Old old time hiword
- RegenSeg DW 0B800h ; Regen buffer segment
- BotLine DW 0 ; Regen offset of bottom line
-
- Main2 PROC near
- cld
- xor ax,ax ; Zero
- mov es,ax
- cmp WORD PTR es:[463h],3D4h ; Check for colour mode
- je GotRegenSeg
- mov RegenSeg,0B000h
- GotRegenSeg: xchg ax,bx ; BX = 0 (page number)
- mov ah,3
- int 10h ; Get cursor position - DH = line
- mov ah,0Fh
- int 10h ; Get video mode
- mov al,ah ; Get screen width
- mul dh ; Calculate offset of bottom line
- shl ax,1 ; Shift for char/attrib
- mov BotLine,ax ; Store
- mov dx,OFFSET InitialMsg
- mov ah,9
- int 21h ; Display initial message
-
- call InitCTC0Mode2 ; Init CTC chan 0 to mode 2, reload 0
-
- MainLoop:
- mov ax,Time1L ; Copy Time1 to Time2
- mov Time2L,ax
- mov ax,Time1M
- mov Time2M,ax
- mov ax,Time1H
- mov Time2H,ax
-
- mov ax,TimeL ; Copy Time to Time1
- mov Time1L,ax
- mov ax,TimeM
- mov Time1M,ax
- mov ax,TimeH
- mov Time1H,ax
-
- call GetTimestamp ; Get timestamp
- mov TimeL,ax ; Store it
- mov TimeM,dx
- mov TimeH,bx
-
- sub ax,Time1L ; Subtract lowords
- sbb dx,Time1M ; Subtract midwords with borrow
- sbb bx,Time1H ; Subtract hiwords with borrow
- jnb Alright ; If no borrow, time didn't go backwards
-
- mov si,OFFSET Time2L ; Oldest time
- mov di,OFFSET Backwards1 ; First string position
- call ToASCII ; Convert to ASCII
- mov si,OFFSET Time1L ; Second-oldest time
- mov di,OFFSET Backwards2 ; Second string position
- call ToASCII ; Convert to ASCII
- mov si,OFFSET TimeL ; New time
- mov di,OFFSET Backwards3 ; Third string position
- call ToASCII ; Convert to ASCII
-
- mov dx,OFFSET BackwardsMsg
- mov ah,9
- int 21h ; Display went-backwards message
-
- Alright: mov si,OFFSET TimeL ; New time
- mov di,OFFSET HexBuffer ; Hex text buffer
- call ToASCII ; Convert to ASCII
- mov si,OFFSET HexBuffer ; ASCII hex text
- mov di,BotLine ; Offset of bottom line of screen
- mov es,RegenSeg ; Regen buffer segment
- mov cx,12 ; Characters to copy
- ScrLoop: movsb ; Copy character
- inc di ; Skip attribute
- loop ScrLoop ; Loop
-
- xor ax,ax
- mov es,ax
- xchg al,BYTE PTR es:[471h]
- test al,al
- js Finish
-
- jmp MainLoop
-
- Finish: mov ax,4C00h
- int 21h ; Terminate with errorlevel 0
- int 20h ; In case DOS-1 (!)
- Main2 ENDP
-
- InitCTC0Mode2 PROC near
- ; Func: Initialise CTC channel 0 to operate in
- ; mode 2 with reload value of 0 (divisor
- ; of 65536, 18.2065 interrupts/second).
- ; Wait for a tick to occur before setting
- ; mode (should minimise disturbance to
- ; system time).
- ; In: None
- ; Out: None
- ; Lost: AX (preserves interrupt flag)
- pushf
- push ds
- sti ; Ensure interrupts are enabled
- xor ax,ax
- mov ds,ax ; Address low memory with DS
- mov ax,ds:[46Ch] ; Get loword of tick count
- WaitTick: cmp ax,ds:[46Ch] ; Changed?
- je WaitTick ; If not, loop
- pop ds
- mov al,00110100b ; Channel 0, mode 2
- cli
- out 43h,al ; Set mode
- xor ax,ax ; Zero
- jmp SHORT $+2 ; Delay
- out 40h,al ; Loword of divisor
- jmp SHORT $+2 ; Delay
- out 40h,al ; Hiword of divisor
- popf ; Restore interrupt flag
- ret
- InitCTC0Mode2 ENDP
-
- PROC GetTimestamp near
- ; Func: Return absolute timestamp (48-bit) in
- ; units of 0.83809534452us since midnight
- ; in the current day (range 000000000000h
- ; to 001800AFFFFFh) using BIOS tick count
- ; variable and CTC channel zero count in
- ; progress, assuming CTC channel 0 is
- ; operating in mode 2 with a reload value
- ; of 0 (65536 divisor).
- ; In: None
- ; Out: AX = Count loword (b0..15) (0000-FFFF)
- ; DX = Count midword (b16..31) (0000-FFFF)
- ; BX = Count hiword (b32..47) (0000-0018)
- ; Lost: AX BX DX
- ; Note: This routine briefly disables then
- ; enables then disables interrupts
- ; regardless of the state of the
- ; interrupt flag on entry.
- ; It restores the original interrupt
- ; flag state on exit.
- push ds ; Preserve register
- push di
- push si
- pushf ; Preserve interrupt flag
- xor ax,ax ; Zero
- mov ds,ax ; Address low memory with DS
- ASSUME ds:nothing ; Not addressing ComFile any more
- cli
- mov si,ds:[46Ch] ; Loword of tick count
- mov di,ds:[46Eh] ; Hiword of tick count
- mov al,00000000b ; Latch count for CTC channel 0
- out 43h,al ; Send it
- jmp SHORT $+2 ; Delay
- in al,40h ; Get lobyte of count
- mov ah,al ; Save in AH
- jmp SHORT $+2 ; Delay
- in al,40h ; Get hibyte of count
- sti ; Make sure interrupts are enabled now
- xchg al,ah ; Get bytes the right way round
- nop ; Sniff for interrupt
- neg ax ; Convert to ascending count
- cli ; No interrupts again for reading count
- mov dx,ds:[46Ch] ; Loword of tick count again
- mov bx,ds:[46Eh] ; Hiword of tick count again
- popf ; Restore original interrupt flag
- cmp dx,si ; Did tick count change?
- je GotTimestamp ; If not, just return second tick count
- test ax,ax ; Is tick count low or high?
- jns GotTimestamp ; If low, read was just past interrupt
- mov dx,si ; If high, previous tick count is right
- mov bx,di ; Get hiword of tick count too
- GotTimestamp: pop si ; Restore working registers
- pop di
- pop ds ; Restore DS
- ASSUME ds:ComFile ; Back to ComFile
- ret
- GetTimestamp ENDP
-
- ToASCII PROC near
- ; Func: Convert a three-word time structure to
- ; 12-digit printable hex representation
- ; In: SI -> Structure
- ; DI -> ASCII buffer in this segment
- ; Out: DI -> Past characters stored
- ; Lost: AX DI ES
- push cs
- pop es ; ES to ComFile
- mov ax,ds:[si+4] ; Get hiword
- call Mach16ToHexAsc ; Convert to hex ASCII representation
- mov ax,ds:[si+2] ; Get hiword
- call Mach16ToHexAsc ; Convert to hex ASCII representation
- mov ax,ds:[si+0] ; Get hiword
- Mach16ToHexAsc PROC near
- push ax
- mov al,ah
- call Mach8ToHexAsc
- pop ax
- Mach8ToHexAsc PROC near
- push ax
- shr al,1
- shr al,1
- shr al,1
- shr al,1
- call Mach4ToHexAsc
- pop ax
- and al,0Fh
- Mach4ToHexAsc PROC near
- add al,90h
- daa
- adc al,40h
- daa
- stosb
- ret
- Mach4ToHexAsc ENDP
- Mach8ToHexAsc ENDP
- Mach16ToHexAsc ENDP
- ToASCII ENDP
-
- ComFile ENDS
- END Main
- -------------------------------- snip snip snip --------------------------------
-
- See all the comments in section »» 9.1 relating to the C program; these
- comments also apply to this program.
-
- ## 9.3 HANDLING THE MIDNIGHT BOUNDARY
-
- The absolute timestamp value returned by the functions in the above programs
- will be in the range 0x000000000000 to 0x001800AFFFFF inclusive. Calculating
- the time difference between two of these timestamps by subtracting the first
- from the second will only give a correct result if the time period did not span
- a midnight boundary. To handle this case, you must check that the second
- timestamp is greater than the first, and if not, add 0x001800B00000 to the
- second timestamp before subtracting them. This will give a correct result,
- provided that no more than about 24 hours has elapsed between the two
- timestamps being taken! (The timestamp value does not include a date).
-
- -------------------------------- snip snip snip --------------------------------
- typedef struct { /* As defined in the sample program */
- unsigned int part;
- unsigned long ticks;
- } timestamp;
-
- /* The following function takes two timestamps in startts and stopts, and
- calculates the time difference and stores them in diffts. The difference
- is in units of 0.8381 us, the same units as the timestamp values. */
-
- void calc_elapsed(timestamp * startts, timestamp * stopts, timestamp * diffts) {
- if (startts->ticks <= stopts->ticks) /* No change of day */
- diffts->ticks = stopts->ticks - startts->ticks;
- else /* Change of day */
- diffts->ticks = stopts->ticks + 0x001800B0L - startts->ticks;
- diffts->part = stopts->part - startts->part;
- if (stopts->part < startts->part)
- --(diffts->ticks);
- return;
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 10 OTHER TOPICS
-
- ## 10.1 THE 586 TIME STAMP COUNTER
-
- In a message in comp.sys.intel and comp.lang.asm.x86 in mid-December 1994,
- Gordon Burditt (gordon@sneaky.lonestar.org) describes a partly undocumented
- instruction available on the Intel 586 (but not guaranteed to be available on
- future Intel processors). The instruction is RDTSC - Read Time Stamp Counter.
- Opcode encoding is 0F 31. It is "unprivileged if bit 2 of CR4 is clear, Ring
- 0 or real mode only if it is set" (whatever that means :-).
-
- This instruction loads the 64-bit Time Stamp Counter register contents into
- EDX:EAX. The Time Stamp Counter is zeroed on power-up and is incremented on
- each CPU clock cycle (e.g. 90 times per microsecond for a 90 MHz CPU - for
- clock doubled or clock tripled processors, does this mean the external clock
- or the internal clock? (*)). This level of resolution is useful for
- performance measurement and CPU usage billing.
-
- The unit of time is system-dependent, and also depends on the accuracy of the
- processor clock, which may not be very good.
-
- Use the CPUID instruction to determine if RDTSC exists on this CPU. EDX
- "feature bits" bit 4 is set if it does.
-
- The Time Stamp Counter register can be written via the documented 586
- instruction WRMSR - Write Model-Specific Register, coding 0F 30. The privilege
- level for this instruction is ring 0 or real mode only. Set ECX to the
- register number (10 hex for the TSC register) and EDX:EAX to the new value and
- execute the instruction.
-
- Use CPUID to determine if WRMSR exists on this CPU. EDX "feature bits" bit 5
- is set if it does. Also, if you are running DOS with EMM386 (i.e. V86 mode),
- you cannot use the privileged instructions.
-
- Thank you Gordon for this information.
-
- Quoting from an article dated Apr 27 1995 in comp.lang.asm.x86 by Philip
- O'Carroll (poc@maths.tcd.ie) with his permission:
-
- >> I can't execute the RDTSC instruction... Is there someone who knows why?
- >
- > 1) The RDTSC instruction cannot be executed from V86 mode. It gives a
- > GPF. I do not know why this is and I have only tested RDTSC from
- > within protected mode.
- >
- > 2) If you are executing it from 16-bit code you will need to use the ADRSIZE
- > prefix to access the upper 16 bits of the EAX and EDX registers.
- >
- > 3) It is possible that the instruction has been disabled by setting the
- > TSD (timestamp disable) bit in CR4. This is unlikely because the Pentium
- > powers up with it clear and I cannot see why an OS would disable it.
-
- Terje Mathisen (Terje.Mathisen@hda.hydro.com) adds, in an article in April
- 1995 in comp.lang.asm.x86:
-
- > RDTSC is by default available for all rings/modes, except V86.
- >
- > The V86 fault was an Intel internal error, i.e. it wasn't supposed to
- > be like that.
-
- The RDTSC instruction causes a GPF if executed in V86 mode. Terje says that
- though this is the documented behaviour, according to an Intel technician the
- RDTSC instruction should have worked in V86 mode too. Terje says that the
- Intel technician also said at the time that RDTSC would work in V86 mode on
- the P6.
-
- > The Time Stamp Disable (TSD) bit in CR4 must be changed (set) to restrict
- > RDTSC to ring 0, so (almost?) all operating systems will let you use the
- > time stamps from ring 3 code.
-
- Philip also sent me the following macro for VC++ 1.5 16-bit (protected mode
- Ring 3 code):
-
- > #define TIMESTAMP(var) __asm \
- > {
- > _asm emit 0x0F \
- > _asm emit 0x31 \
- > _asm emit 0x66 \
- > _asm mov word ptr var, ax \
- > _asm emit 0x66 \
- > _asm mov word ptr var[+4], dx \
- > }
- >
- > Usage:
- >
- > DWORD timest[2];
- >
- > TIMESTAMP(timestamp);
-
- Philip also told me he has written a Windows VxD for accessing the profiling
- counters from Ring 3 code, but I don't know where, or when, it will be
- available.
-
- > My VxD allows Windows apps to access the Pentium profiling registers
- > detailed in Byte July 1994. Specifically there are two counters which
- > can count various different processor events such as instructions
- > executed, data cache hits/misses etc.
- >
- > The TSC _can_ be used by Windows apps without recourse to a VxD.
-
- Thanks guys.
-
- ## 10.2 SERIAL PORT REGULAR INTERRUPT
-
- If your application will have a spare serial port to play with, it can generate
- a regular interrupt using the Transmit interrupt facility on the serial chip
- (known as a UART, for Universal Asynchronous Receiver/Transmitter). There are
- other ways to make the UART generate interrupts, but the Transmit interrupt is
- easiest to use.
-
- UARTs usually drive IRQ4 and IRQ3. These interrupts are reserved for COM1 and
- COM2 respectively. When COM3 and COM4 are present, they sometimes 'share' IRQ4
- and IRQ3 respectively, with COM1 and COM2, but this 'sharing' only works if the
- ports are not used simultaneously (except on MicroChannel machines and possibly
- on EISA machines, where proper interrupt sharing is possible with the right
- software support). In some cases, the otherwise spare interrupt lines, such as
- IRQ5 and IRQ2/9, are used for COM3 and COM4.
-
- ## 10.2.1 SERIAL PORT (UART) DOCUMENTATION
-
- This information is brief and incomplete. There are many books and electronic
- documents which describe the UART much more thoroughly, such as Chris Blum's
- "The Serial Port" FAQ which is posted periodically in the Internet newsgroup
- comp.os.msdos.programmer.
-
- There are several types of UARTs. The basic device is the INS8250 which was
- originally developed by National Semiconductor. It is not an Intel device,
- despite the number. Descendants such as the 8250A, 16C450, and 16C550 add
- features, improve performance, and/or correct design errors in previous
- versions of the chip.
-
- The UART occupies eight consecutive I/O addresses starting at the I/O Base
- address. The I/O Base address of a nominated UART (e.g. COM1) can be found
- in the table in the BIOS data area in low memory, starting at 0040:0000 (aka
- 0000:0400). The table has four entries, at 0, 2, 4, and 6, which correspond
- to COM1, COM2, COM3, and COM4 respectively. If the value is zero, there is
- no such port. If nonzero, it specifies the I/O Base address of that port.
-
- The registers in the UART are as follows.
-
- I/O address Access Name Description
- ----------- ------ ---- -----------
-
- IOBase+0 Read RDR Received data (DLAB=0)
- Write TDR Transmit data (write) (DLAB=0)
- Read/write BRDL Divisor register lobyte (DLAB=1)
- IOBase+1 Read/write IER Interrupt Enable Register (DLAB=0)
- Read/write BRDH Divisor register hibyte (DLAB=1)
- IOBase+2 Read-only IIR Interrupt Identification Register
- Write-only FCR FIFO control register (FIFO UARTs only)
- IOBase+3 Read/write LCR Line Control Register
- IOBase+4 Read/write MCR Modem Control Register
- IOBase+5 Read-only LSR Line Status Register
- IOBase+6 Read-only MSR Modem Status Register
- IOBase+7 Read/write Scratch register (on some UARTs only)
-
- The 'DLAB' above is the Divisor Latch Access Bit, which is bit 7 of the Line
- Control Register (LCR) at IOBase+3. This bit controls access to the divisor
- register (hence the name). The divisor register is a 16-bit register which
- acts as a divisor to determine the baud rate. It is accessible at IOBase+0
- (lobyte) and IOBase+1 (hibyte) when the DLAB is set. When the DLAB is clear,
- the transmit and receive data register and the IIR appear at these I/O
- locations.
-
- The relevant registers are now described briefly.
-
- IER 7 6 5 4 3 2 1 0 IOBase+1, read/write
- * * . * . . . . Not used; zero
- . . * . . . . . Special function enable (some UARTs)
- . . . . * . . . Modem Status Change Interrupt Enable (1=enable)
- . . . . . * . . Line Status Change Interrupt Enable (1=enable)
- . . . . . . * . Transmit Ready Interrupt Enable (1=enable)
- . . . . . . . * Received Data Interrupt Enable (1=enable)
-
- IIR 7 6 5 4 3 2 1 0 IOBase+2, read-only
- * * . . . . . . FIFOs Enabled flags (FIFO UARTs only)
- . . * * . . . . Special function status (some UARTs)
- . . . . * * * . Interrupt Identification bits 2, 1, and 0
- . . . . . . . * Interrupt output active (0=active, 1=inactive)
-
- LCR 7 6 5 4 3 2 1 0 IOBase+3, read/write
- * . . . . . . . Divisor Latch Access Bit (DLAB)
- . * . . . . . . Set Break (1=break, 0=normal)
- . . * . . . . . Stick Parity (1=stick, 0=normal parity, if enabled)
- . . . * . . . . Even Parity (1=even, 0=odd, if enabled)
- . . . . * . . . Parity Enable (1=enable, 0=disable)
- . . . . . * . . Stop bits (1=1.5/2, 0=1 stop bits)
- . . . . . . * * Word length (00=5, 01=6, 10=7, 11=8 data bits)
-
- LSR 7 6 5 4 3 2 1 0 IOBase+5, read-only
- * . . . . . . . Not used; 0
- . * . . . . . . TSRE - Transmit Shift Register Empty
- . . * . . . . . THRE - Transmit Holding Register Empty
- . . . * . . . . BI - Break interrupt (break received)
- . . . . * . . . FE - Framing Error
- . . . . . * . . PE - Parity Error
- . . . . . . * . OR - Overrun error
- . . . . . . . * DR - Data Ready (received a data byte)
-
- MCR 7 6 5 4 3 2 1 0 IOBase+4, read/write
- * . . . . . . . Special function enable (some UARTs)
- . * * . . . . . Not used; zero
- . . . * . . . . Loopback enable (1=enable)
- . . . . * . . . OUT2 (interrupt buffer control) (1=active)
- . . . . . * . . OUT1 (1=active)
- . . . . . . * . RTS - Request To Send (1=active)
- . . . . . . . * DTR - Data Terminal Ready (1=active)
-
- MSR 7 6 5 4 3 2 1 0 IOBase+6, read-only
- * . . . . . . . DCD - Data Carrier Detect (0=inactive, 1=active)
- . * . . . . . . RI - Ring Indicator (0=inactive, 1=active)
- . . * . . . . . DSR - Data Set Ready (0=inactive, 1=active)
- . . . * . . . . CTS - Clear To Send (0=inactive, 1=active)
- . . . . * . . . DDCD - Delta DCD (0=no change, 1=changed)
- . . . . . * . . TERI - Trailing Edge Ring Indicator (1=edge)
- . . . . . . * . DDSR - Delta DSR (0=no change, 1=changed)
- . . . . . . . * DCTS - Delta CTS (0=no change, 1=changed)
-
- Bit 2 of the LCR controls the number of stop bits. If this bit is 0, one stop
- bit is used. If this bit is 1, two stop bits are used, except when the word
- length bits are both zero (i.e. 5-bit word length), in which case 1.5 stop bits
- are used.
-
- Bit 3 of the MCR (OUT2) controls the tristate buffer that drives the interrupt
- line. When the port is in use, and the interrupt facility is required, this
- bit must be set, to enable the buffer to drive the IRQ line on the slot bus.
-
- The baud rate divisor is chosen as 115200 divided by the baud rate. For
- example if a baud rate of 9600 bits per second is required, the divisor value
- is 115200/9600, which is 12. Both lobyte and hibyte must be programmed. The
- DLAB must be set prior to writing the divisor, and turned off afterwards.
-
- For a ten-bit character length (e.g. 8-bit data with no parity, or 7-bit data
- with parity), the transmitter will generate a transmit ready interrupt ten
- times slower than the bit rate, e.g. 960 times per second in the above example.
-
- The serial port interrupt must be enabled on the PIC for interrupt driven
- operation (see section »» 6.10 for details).
-
- There are four independently controllable interrupt sources in the UART. They
- correspond to bits 3-0 of the IER. When handling interrupts from the UART when
- more than one interrupt source is enabled in the IER, particularly if the modem
- status change interrupt is enabled, your software must take care to ensure that
- all interrupt sources are acknowledged before sending the EOI command to the
- PIC. This condition can be detected by checking bit 0 of the Interrupt
- Identification Register (IIR) - if this bit is zero, then an unacknowledged
- interrupt source is still pending. This will be one of the interrupt sources
- that are enabled via the IER. This condition must be cleared before the EOI
- is sent.
-
- The Received Data interrupt is cleared when the character is read from the
- Received Data register. The Transmit Ready interrupt is cleared when any
- character is written to the Transmit Data register. The Line Status Change
- and Modem Status Change interrupts are cleared by a read of the LSR and the
- MSR, respectively.
-
- The program in section »» 10.2.2 demonstrates how to use a serial port as a
- regular interrupt source.
-
- ## 10.2.2 SAMPLE PROGRAM: REGULAR INTERRUPT USING THE SERIAL PORT
-
- This program uses a serial port (COM1 in this case) to generate a regular
- (periodic) interrupt. The UART divisor is set to 96, giving a baud rate of
- 1200 bps. At ten bits per character, the UART will transmit a character 120
- times per second, and generate the Transmit Ready interrupt at the same rate.
-
- This program has the IRQ and interrupt numbers, and the serial port's I/O Base
- address, hard-coded via #defines. These could be set by command line options
- and/or determined via the table of addresses at 0000:0400 described earlier.
-
- Note that while this program is running, it will be transmitting characters
- out the serial port at 1200 baud. If a serial printer, or any other device,
- is connected to the serial port, you might want to remove it before running
- this program!
-
- See section »» 10.2.3 for a method of incorporating this timing technique into
- a program that is already using the serial port, to implement delays in a
- transmitted data stream.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #17
- Demonstrates regular interrupts using the serial port
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save and assemble the critical error module CRIT_ERR
- Save this sample code to SAMPLE17.C
- Compile this module with:
- bcc -c -I<inc_path> -ms sample17.c
- Link the modules with:
- tlink /c /x <c0_path>\c0s.obj sample17.obj crit_err.obj,
- sample17, nul, <lib_path>\cs
- Where inc_path is the path to your C header files, c0_path is the path to your
- startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #pragma inline; /* Required for asm pushf, popf, and cli */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <dos.h> /* Needed for MK_FP */
- #include <io.h> /* Needed for _write() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDERR 2 /* DOS handle for standard error */
-
- #define BAUDDIV 96 /* Interrupt rate = 11520 / BAUDDIV */
-
- #define IOBASE 0x3F8 /* COM1 standard I/O base address */
- #define COMIRQ 4 /* IRQ number for COM1 (standard) */
- #define COMINT 0x0C /* Corresponding interrupt number */
- #define PICMASK (1 << COMIRQ) /* Bitmask for interrupt in PIC IMR */
-
- void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
- unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
-
- typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
-
- intfuncp old_com_int = (intfuncp)0xFFFFFFFFL;
-
- static unsigned int onetwentieths = 0; /* 120ths of seconds */
- static unsigned int seconds = 0; /* Seconds */
- static unsigned char old_brdl, old_brdh; /* Old baud rate divisor */
- static unsigned char old_lcr, old_mcr, old_ier; /* Old LCR, MCR, IER contents */
-
- /* The interrupt handler is invoked when the UART is transmit ready. It must
- feed the UART to shut it up. When the UART is hungry again, it will issue
- another interrupt. This handler increments a counter variable. */
-
- void interrupt com_int_handler(void) {
- outportb(IOBASE, 0x00); /* "Feeeed me Seymour" */
- if (++onetwentieths >= 120) { /* Increment 120ths count */
- onetwentieths = 0;
- ++seconds;
- }
- outportb(0x20, 0x20); /* Send EOI */
- return; /* From interrupt */
- }
-
- void restore_normal(void) {
- asm pushf;
- asm cli;
- outportb(0x21, inportb(0x21) | PICMASK); /* Disable int in PIC */
- outportb(IOBASE + 3, old_lcr & 0x7F); /* Clear DLAB */
- outportb(IOBASE + 1, old_ier); /* Restore IER */
- outportb(IOBASE + 3, old_lcr | 0x80); /* Set DLAB */
- outportb(IOBASE + 0, old_brdl); /* Lobyte of divisor */
- outportb(IOBASE + 1, old_brdh); /* Hibyte of divisor */
- outportb(IOBASE + 3, old_lcr); /* Restore LCR */
- outportb(IOBASE + 4, old_mcr); /* Restore MCR */
- asm popf;
- return;
- }
-
- void abort_cleanup(int dos_is_safe) {
- if (dos_is_safe) {
- if (old_com_int != (intfuncp)0xFFFFFFFFL) {
- setvect(COMINT, old_com_int);
- old_com_int = (void far *)0xFFFFFFFFL;
- }
- }
- else {
- if (old_com_int != (intfuncp)0xFFFFFFFFL) {
- *((intfuncp far *)MK_FP(0, COMINT << 2)) = old_com_int;
- old_com_int = (void far *)0xFFFFFFFFL;
- }
- }
- restore_normal();
- return;
- }
-
- void interrupt ctrl_c_handler(void) {
- static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
- if (is_at_crit_prompt())
- abort_cleanup(FALSE);
- else {
- abort_cleanup(TRUE);
- _write(STDERR, &message, sizeof(message));
- }
- exit(255);
- }
-
- void poll_exit(void) {
- if (bioskey(1)) {
- if ((bioskey(0) & 0xFF) == 27) {
- setvect(COMINT, old_com_int);
- old_com_int = (void far *)0xFFFFFFFFL;
- restore_normal();
- exit(0);
- }
- }
- return;
- }
-
- void main(void) {
- unsigned int main_onetwentieths, main_seconds, old_onetwentieths;
-
- printf("Sample program #17 - Demonstrates regular interrupts using the serial port\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
- printf("Press <Esc> to exit\n\n");
-
- crit_err_intercept(); /* Trap critical errors */
- setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
- old_com_int = getvect(COMINT);
- setvect(COMINT, com_int_handler);
-
- asm cli;
- old_lcr = inportb(IOBASE + 3); /* Get old LCR value */
- old_mcr = inportb(IOBASE + 4); /* Get old MCR value */
- outportb(IOBASE + 3, 0x83); /* Set DLAB */
- old_brdl = inportb(IOBASE + 0); /* Get divisor lobyte */
- old_brdh = inportb(IOBASE + 1); /* Get divisor hibyte */
- outportb(IOBASE + 0, BAUDDIV & 0xFF); /* Set up divisor lobyte */
- outportb(IOBASE + 1, BAUDDIV >> 8); /* Set up divisor hibyte */
- outportb(IOBASE + 3, 0x03); /* Clear DLAB */
- old_ier = inportb(IOBASE + 1); /* Get old IER value */
-
- outportb(IOBASE + 4, 0x08); /* Enable interrupt buffer */
- /* Use 0x18 instead of 0x08 above to set loopback mode so
- data is not transmitted out the serial port connector */
- outportb(IOBASE + 1, 0x00); /* No interrupts yet */
- outportb(0x21, inportb(0x21) & (~PICMASK)); /* Enable int in PIC */
- outportb(IOBASE + 1, 0x02); /* Enable Tx interrupt */
-
- asm sti;
-
- printf("Seconds 120ths\n");
-
- while (1) {
- asm cli;
- main_onetwentieths = onetwentieths;
- main_seconds = seconds;
- asm sti;
- if (main_onetwentieths != old_onetwentieths) {
- printf("%5d %3d\r",
- main_seconds, main_onetwentieths);
- old_onetwentieths = main_onetwentieths;
- }
- poll_exit();
- }
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 10.2.3 INSERTING DELAYS INTO SERIAL PORT TRANSMITTED DATA
-
- The information and code given in this section is untested.
-
- In controller applications, it is sometimes necessary to insert delays into a
- serial transmission. This may be required as part of a communication protocol,
- or for other reasons. For example, certain types of modems used in medium
- speed data communication cannot accept data to be transmitted immediately when
- the transmit enable flow control line is driven active by the computer, so the
- computer must raise flow control and delay for a certain time (usually in the
- order of 5-20 milliseconds) before starting to transmit data.
-
- These delays can be created using the transmit interrupt method, using the same
- serial port which transmits the data, via the loopback enable bit, bit 4 of the
- MCR (see section »» 10.2.1). Setting this bit forces the UART's data output in
- the idle (marking) state, and loops its transmitted data back to its receiver,
- internally to the UART chip. In this state, the transmit ready interrupt can be
- used to time the delay period as per the sample program in section »» 10.2.2.
- When the required number of interrupts have occurred, i.e. the required delay
- time has elapsed, wait for the last byte to be serialised (by waiting for TSRE
- in the LSR to go true) and then turn off loopback mode. Your program can then
- begin transmitting.
-
- This method cannot be used if your program must be able to receive characters
- during the delay period, because in loopback mode, the UART ignores the receive
- data signal, but this method can be used in half duplex applications. Also,
- the granularity of the delay is one character length. Reprogramming the baud
- rate during the delay period might allow finer delay timing, but this would be
- very technical, if not impossible, to implement correctly.
-
- If your program transmits under interrupt, I would suggest using some flags to
- communicate between the mainline and the interrupt handler. For example, the
- mainline could signal the start of a transmission by enabling outgoing flow
- control, selecting loopback mode, sending one or two characters to the UART,
- setting an 'idle-leader' flag to be used by the interrupt routine, and enabling
- transmit interrupts. The interrupt routine would check the interrupt source
- (if more than one source is enabled on the UART) and if the interrupt is due to
- transmit ready, first check whether the idle-leader flag is set, and if so,
- send any character to the port (e.g. 0FF hex), decrement the idle leader
- counter, and return. If the idle leader timer counts down to zero, either the
- mainline or the interrupt routine would have to wait for TSRE to go active,
- turn off loopback mode, and start transmitting data.
-
- If your program is doing nothing while it waits during the transmit idle period,
- and does not otherwise transmit under interrupt, you can use the transmit ready
- interrupt signal without actual interrupts, in a polled fashion. Fast response
- to a transmit ready signal is not necessary, as there is a window of about two
- character lengths between when the THRE (Transmit Holding Register Empty) signal
- goes true, and when the transmit data register must be filled, due to the double
- buffering provided by the transmit holding register and transmit shift register.
-
- Here is a crude, untested function to transmit a string of data bytes without
- using interrupts. It asserts outgoing flow control (DTR and RTS), and waits
- for a number of character-periods determined by the leader_len parameter, then
- transmits the message pointed to by the msg parameter for the number of bytes
- specified by the msg_length parameter, waits for the last character to be
- fully serialised plus nearly one character length, and drops the RTS line.
-
- A similar method can be used with an interrupt handler, with quite a lot of
- extra mucking around.
-
- void wait_tx_string(unsigned leader_len, char * msg, unsigned msg_length) {
- while ((inportb(IOBASE+5) & 0x60) != 0x60)
- ; /* Wait for last char to be serialised */
- asm pushf;
- asm cli;
- outportb(IOBASE+4, inportb(IOBASE+4) | 0x13); /* DTR, RTS, loopback */
- asm popf;
- while (leader_len--) {
- outportb(IOBASE, 0xFF); /* Dummy byte */
- while ((inportb(IOBASE+5) & 0x20) == 0)
- ; /* Wait for THRE again */
- }
- while ((inportb(IOBASE+5) & 0x40) == 0)
- ; /* Wait for TSRE */
- asm pushf;
- asm cli;
- outportb(IOBASE+4, inportb(IOBASE+4) & 0xEF); /* Loopback off */
- asm popf;
- while (msg_length--) {
- outportb(IOBASE, *(msg++));
- while ((inportb(IOBASE+5) & 0x20) == 0)
- ; /* Wait for THRE between chars */
- }
- while ((inportb(IOBASE+5) & 0x40) == 0)
- ; /* Wait for last char sent */
- asm pushf;
- asm cli;
- outportb(IOBASE+4, inportb(IOBASE+4) | 0x10); /* Loopback back on */
- asm popf;
- outportb(IOBASE, 0xFF); /* Dummy byte */
- while ((inportb(IOBASE+5) & 0x60) != 0x60)
- ; /* Wait for dummy char to be serialised */
- asm pushf;
- asm cli;
- outportb(IOBASE+4, inportb(IOBASE+4) & 0xED); /* RTS, loopback off */
- asm popf;
- return;
- }
-
- This is only an outline of this technique. If you are implementing this system,
- I would strongly recommend a thorough read of a technical document on the serial
- port, such as Chris Blum's article (see section »» 10.2.1) or manufacturers'
- data sheets for the serial chips, so you can determine all the implications of
- your code's actions. This is particularly important if timing is very critical,
- as there are timing subtleties and interactions between the transmit holding
- register and the transmit shift register that must be taken into account.
-
- There is also a problem caused by the fact that a transmit ready interrupt is
- acknowledged by a read of the LSR. This has serious implications relating to
- when the LSR may be interrogated. If the mainline accesses the LSR, it may
- clear a pending interrupt condition, causing transmit interrupts to cease.
- I have not investigated this properly, but be warned! (*)
-
- ## 10.3 EXTERNAL INTERRUPT SOURCES
-
- An external interrupt source can be used for many things, including timekeeping.
- External hardware of some sort will normally be required to drive the interrupt
- in the desired way. Usually the external interrupt source will use the parallel
- port or the serial port to get access to an interrupt level (IRQ) on the slot
- bus.
-
- The parallel or serial port input can be driven by an external source at the
- desired rate. If only a slow interrupt rate is required, you can clock the
- input at 300 Hz, which can be derived using a PLL (Phase Locked Loop) from the
- mains frequency. 300 Hz is a good choice because it can be generated from both
- 50Hz (Europe) and 60Hz (America) mains frequencies. Thanks to John Stockton
- for suggesting this technique (though he points out that he has not tested it).
-
- You may have noticed how you never have to adjust clocks that are mains powered
- (except after power loss, of course). This is because the mains frequency is
- usually regulated very carefully by power supply authorities and, though it may
- vary slightly in the short term, its long term accuracy should be very high.
- A frequency derived from the mains in this way could be a good clock source
- for timing applications which require high long-term accuracy.
-
- ## 10.3.1 EXTERNAL INTERRUPT THROUGH PARALLEL PORT
-
- The parallel port interrupt is normally connected to IRQ7 although some cards
- are jumper-selectable to IRQ5 and maybe other IRQs. The parallel port interrupt
- was intended to be used in the normal course of sending data to a parallel
- printer, but DOS and BIOS do not use the interrupt facility. Versions of OS/2
- prior to Warp (3.0) did require the interrupt for printing, but from Warp
- onwards the interrupt is not required (though it can be used if the /IRQ
- switch is provided on the line in CONFIG.SYS, i.e. BASEDEV=PRINT0x.SYS /IRQ).
-
- The basic parallel port consists of three registers at consecutive I/O
- locations starting at the I/O Base address. The I/O Base address of a
- nominated LPT port (e.g. LPT1) can be found in the table in the BIOS data
- area in low memory, starting at 0040:0008 (aka 0000:0408). The table has
- three entries, at 8, 0A, and 0C, which correspond to LPT1, LPT2, and LPT3.
- If the value is zero, there is no such port. Some BIOSes may support a fourth
- port base entry at 0E, but other BIOSes use this location for an unrelated
- function.
-
- The register at IOBase+2 is the Control register. Bit 4 of this register
- controls the tristate buffer that drives the IRQ line, and the buffer is
- enabled if the bit is set. In this state, a falling edge (high to low
- transition) on the Ack signal (pin 10 of the 25-pin connector) will cause an
- interrupt (providing that the interrupt is enabled in the PIC's IMR; see
- section »» 6.10).
-
- ## 10.3.2 EXTERNAL INTERRUPT THROUGH SERIAL PORT
-
- In addition to the Transmit Ready interrupt (which can provide a regular
- interrupt source, see section »» 10.2 and subsections), the serial port can
- issue an interrupt when received data is available, when the receiver line
- status changes, and/or when the receiver 'modem status' changes. The 'modem
- status' refers to the four incoming flow control lines on the serial connector
- which indicate the modem status when the port is connected to a modem.
- These inputs are as follows.
-
- Name Full name Pin (9-pin) Pin (25-pin)
-
- CTS Clear To Send 8 5
- DSR Data Set Ready 6 6
- RI Ring Indicator 9 22
- DCD Data Carrier Detect 1 8
-
- The modem status change interrupt is enabled by bit 3 of the Interrupt Enable
- Register (IER) (see section »» 10.2.1 for details). When this interrupt is
- enabled, and the interrupt buffer is enabled via the OUT2 line in the Modem
- Control Register (MCR) (also see section »» 10.2.1) and the appropriate IRQ is
- enabled via the IMR in the PIC (see section »» 6.10), every transition on any
- of these four incoming lines will cause an interrupt request.
-
- The current states of the four incoming lines can be read on the Modem Status
- Register (see section »» 10.2.1) which also contains the 'delta' signals, which
- indicate whether the corresponding line has changed state since the last time
- the MSR was read. When using these signals, remember that they clear when your
- program reads the MSR, so read the MSR once only, and test the delta bits in
- this value - don't re-read the MSR to check for any other delta bits, as they
- will all have cleared just after the MSR was read the first time.
-
- See the notes in section »» 10.2.1 about ensuring that all interrupt sources
- are acknowledged before leaving the interrupt routine.
-
- ## 10.3.3 EXTERNAL INTERRUPT THROUGH SOUND CARD
-
- Sound cards such as the Sound Blaster can most probably generate periodic
- interrupts, though these are usually used for some purpose related to sound
- generation, not for timing in the general sense. I haven't investigated this
- one. Get a technical reference such as the Sound Blaster Freedom project if
- you want to try this.
-
- ## 10.3.4 EXTERNAL INTERRUPT THROUGH CUSTOM I/O CARD
-
- There are many third party I/O cards that are able to generate periodic
- interrupts for various purposes, and for one-off dedicated applications or for
- experimenting, you may wish to use these. I have no references, but you could
- try looking through advertisements in computer experimenters' magazines for
- sources.
-
- Alternatively, if you have the time, money, experience, and inclination, you
- can make your own I/O card. Interrupt lines on the ISA bus are all rising edge
- triggered. Just generate the rising edge, and if there is no other card driving
- that line, and the interrupt is enabled in the mask register of the appropriate
- PIC, the appropriate interrupt will be invoked. On ISA cards it seems to be
- standard practice to drive IRQ lines with a buffer that can be put into high
- impedance mode (tri-stated) or driving mode, under software control. While
- this is doesn't allow for interrupt sharing, or have any other great purpose,
- it is in general not a bad idea.
-
- The EIDE, MCA, and PCI busses will be different. Get a good technical book if
- you intend to try this.
-
- ## 10.4 THE JOYSTICK PORT
-
- The joystick port, or game port, is accessed via a single I/O location, which
- is normally at I/O address 201h (may be jumper-settable to 301h on some cards).
- The joystick standard joystick hardware interface circuit is given in Figure 4
- in the FIGURES archive. It supports four pushbutton-type inputs without
- hardware debouncing, and four variable resistors (potentiometers, abbreviated
- 'pot') for position sensing, to support two joysticks, each with two buttons
- and two pots (for the X and Y axes). Some cards support only the first
- joystick (see later).
-
- ## 10.4.1 JOYSTICK PORT HARDWARE
-
- The joystick hardware cannot generate an interrupt, and has no outputs, though
- it does provide a +5V supply which can be used externally, which the parallel
- port does not have. It is really only useful as a general purpose input port.
-
- The pots are read using four independent monostable or 'one-shot' circuits.
- The monostable circuits are triggered by a signal from the processor, and each
- one charges or discharges a capacitor at a rate determined by the resistance of
- the associated pot. When triggered, the monostable's output goes high. When
- the capacitor reaches a certain voltage, the output returns low, and remains
- low until the monostable is next triggered by the processor. Thus the name,
- 'one-shot'. The processor triggers the monostable, then measures the length of
- time taken for the monostable's output to go low, to determine the resistance,
- and thus the position, of the pot. The formula relating resistance to time is
- supposedly: T = 24.2 + (0.011 x R) where T is the time in microseconds and R is
- the resistance of the pot in ohms, but the capacitors are usually inaccurate
- (+/- 20% or worse) ceramic components, and are influenced by temperature, so
- the above formula is 'nominal' only. In practice the relationship will vary
- from one input to the next, and depend on temperature.
-
- The nominal pot end-to-end resistance is 100 kilohms (100000 ohms), giving a
- nominal maximum timeout of about 1125 us. Times in this range can be measured
- accurately using CTC channel zero or two in either mode 3 or mode 2, or using
- Refresh Detect. A sample program to read the joystick position is given in
- section »» 10.4.2.
-
- The joystick connector is a 15-pin female D-sub connector. The pinout is:
-
- Pin Dir Type Stick Button Axis Return to
-
- 1 Out +5V
- 2 In Btn A 1 Gnd
- 3 In Pot A X +5V
- 4 - Gnd
- 5 - Gnd
- 6 In Pot A Y +5V
- 7 In Btn A 2 Gnd
- 8 Out +5V
- 9 Out +5V
- 10 In Btn B 1 Gnd
- 11 In Pot B X +5V
- 12 - Gnd
- 13 In Pot B Y +5V
- 14 In Btn B 2 Gnd
- 15 Out +5V
-
- Writing any value to the I/O port (201h or 301h) causes all four monostables to
- start timing. Their outputs go high immediately, and go low a certain length
- of time later, depending on the resistance of the associated potentiometer.
- Reading the I/O port yields the following:
-
- 7 6 5 4 3 2 1 0
- * . . . . . . . Button B2 (pin 14), 0=closed, 1=open (default)
- . * . . . . . . Button B1 (pin 10), 0=closed, 1=open (default)
- . . * . . . . . Button A2 (pin 7), 0=closed, 1=open (default)
- . . . * . . . . Button A1 (pin 2), 0=closed, 1=open (default)
- . . . . * . . . Monostable BY (from pin 13), 1=timing, 0=timed-out
- . . . . . * . . Monostable BX (from pin 11), 1=timing, 0=timed-out
- . . . . . . * . Monostable AY (from pin 6), 1=timing, 0=timed-out
- . . . . . . . * Monostable AX (from pin 3), 1=timing, 0=timed-out
-
- Some cards only support one joystick. You may be able to tell by looking for
- a 14-pin chip with '556' in its part number (single joystick), or a 16-pin chip
- with '558' in its part number (two joysticks), usually located near the 15-pin
- connector. Some cards implement the joystick interface in an ASIC, in which
- case you may be able to follow tracks to find how many joysticks are supported.
-
- ## 10.4.2 READING THE JOYSTICK BUTTONS AND POSITION
-
- Most BIOSes apart from very early ones provide functions to read the buttons
- and positions of the joystick, accessed via int 15h. If the function is not
- supported, carry is set on return and AH may be set to 80h or 86h. Steve
- McGowan and Mark Feldman in their PC-GPE article say that many machines do not
- support the BIOS functions properly, and that the first function (read buttons)
- may be supported, while the second function (read positions) may not.
-
- Read Joystick Buttons : int 15h
- Call with: AH = 84 hex
- DX = 0000 hex
- Returns: AL = Button states in bits 7-4, as read from input port
-
- Bits 7-4 are valid in the returned value, and they default to '1' and are '0'
- if the corresponding button is currently depressed. This function does not
- perform any debouncing on the joystick button inputs. This means that the
- bit may 'bounce' (i.e. alternate randomly, one or more times) at the instant
- that it makes or breaks contact, because of the mechanical nature of the
- switch.
-
- Read Joystick Positions : int 15h
- Call with: AH = 84 hex
- DX = 0001 hex
- Returns: AX = Joystick A, axis X (0-511, 0 if timed-out)
- BX = Joystick A, axis Y (0-511, 0 if timed-out)
- CX = Joystick B, axis X (0-511, 0 if timed-out)
- DX = Joystick B, axis Y (0-511, 0 if timed-out)
-
- This function reads each of the four inputs separately, disabling interrupts for
- a few milliseconds each time. It may use CTC channel 0 for timing, and if so,
- its calculations will be affected if CTC channel 0 is operating in a different
- mode from the mode that the BIOS is expecting (e.g. if the BIOS POST set CTC
- channel 0 to mode 3, and a program has subsequently reprogrammed it for mode 2,
- or vice versa) or if CTC channel 0 is operating with a non-standard divisor.
- Inputs which have no joystick connected will time out and be reported as zero.
-
- ## 10.4.3 NOTES FROM THE PC-GPE ARTICLE
-
- In the joystick article in the PC Games Programmer's Encyclopedia (PC-GPE),
- Steve McGowan and Mark Feldman give some useful information.
-
- All joysticks they tested returned non-linear values, i.e. the value returned
- at centre-position is not half way between the values returned at corner
- positions, so most joystick setup programs require the user to set up the
- centre position as well as the corner positions. (This is not surprising, as
- joysticks apparently use logarithmic, not linear, potentiometers!) They suggest
- a 10% 'dead zone' around the centre, as joysticks do not always centre
- repeatably. Joysticks are not high quality devices, and some smoothing (e.g.
- 1/4 new plus 3/4 old, or 1/8 new + 7/8 old) on the position values may help.
-
- ## 10.4.4 SAMPLE PROGRAM: READING THE JOYSTICK POSITION
-
- The following program demonstrates three methods of reading the joystick pot
- positions.
-
- The first method, ctc2_read_joystick(), uses CTC channel 2 in mode 0 for timing
- the pulse produced by the joystick hardware, and also detecting timeout.
- Timeout occurs when more than MAXCTCCLOCKS CTC clocks pass and the monostable
- output is still active. This method reads one joystick input at a time.
-
- The second method, refd_read_joystick(), uses the Refresh Detect signal (see
- section »» 7.37) as the timing source, and reads all four inputs simultaneously.
-
- The third method uses the BIOS function call.
-
- The first method has two caveats: (1) the ctc2_read_joystick() function will
- cut off any audio being generated using CTC channel 2, and (2) T2PORT is 0x62
- on PCs and XTs with the old 8255 chip (see section »» 7.30) so if you wish to
- support these machines, you will have to detect the machine type (code for this
- is included in section »» 7.37.1 but is in assembler) and select the port
- address accordingly. It may work under OS/2. HW_TIMER should be set ON.
-
- The ctc2_read_joystick() function uses a similar technique to the equivalent
- BIOS function, though I wrote it before I disassembled the BIOS version. It
- has several advantages over the BIOS function - it doesn't rely on the mode
- and divisor of CTC channel zero, so it will work if CTC channel zero has been
- programmed with a different mode and/or a different divisor, it has much more
- consistent and more accurate timeout detection, it reads one input at a time
- (so only the relevant inputs need be read), it will often be quicker than the
- BIOS function, and it has higher resolution. Its major disadvantage is that
- because it uses CTC channel 2, it will stop any speaker sound that may be in
- progress when the function is called (sound cards are not affected, of course).
-
- In a practical application, the values returned from ctc2_read_joystick() could
- be averaged (using a 3/4-old-averaged-value plus 1/4-new, or 7/8-old-averaged-
- value plus 1/8-new, or similar algorithm, or average of last n samples) to
- reduce jitter, though this will slow the response.
-
- The second method, using Refresh Detect, will not work on a PC or XT, as they
- do not have a Refresh Detect signal. Also, it assumes that the refresh rate
- is as configured by the BIOS, i.e. a divisor of 18 in CTC channel 1, giving one
- refresh every 15.0857 microseconds. It has a much lower resolution than the
- first method, but has the advantage that it reads all four inputs at once, so
- in most cases will be the quickest method, and it does not make any use of CTC
- channels 0 and 2, so it does not rely on the programmed mode and divisor in
- CTC channel 0, and does not disrupt speaker sound being generated via CTC
- channel 2.
-
- This sample program also reads the joystick using the BIOS function described
- in the previous section, and displays the values read directly and the values
- read via the BIOS.
-
- See section »» 6.22 for the explanation of the pushf/cli/popf technique.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #18
- Demonstrates three ways of reading the joystick
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save this file to SAMPLE18.C and compile with:
- bcc -I<inc_path> -L<lib_path> -ms sample18.c
- Where inc_path is the path to your C header files and your startup modules
- C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB.
-
- */
-
- #pragma inline; /* Required for asm pushf, popf, cli, and sti */
-
- #include <bios.h> /* Needed for bioskey() */
- #include <dos.h> /* Needed for inportb() and outportb() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL)
-
- #define JOYPORT 0x201 /* Joystick port I/O address */
- #define MONOS 0x0F /* Bottom four bits are monostable outputs */
-
- #define T2PORT 0x61 /* Use 0x62 for PC and XT! */
- #define T2OUT 0x20 /* Bit 5 is timer 2 output readback */
-
- #define PORTB 0x61 /* For Refresh Detect - AT only! */
- #define REFDET 0x10 /* Bit 4 is Refresh Detect */
-
- #define MAXCTCCLOCKS 1800 /* Max CTC clocks for timeout */
- #define WAITCTCCLOCKS 10 /* CTC clocks for monostable recovery (<255) */
- #define MAXREFRESH 100 /* Maximum refresh detect counts for timeout */
-
- typedef struct {
- int ax;
- int ay;
- int bx;
- int by;
- } joyvals;
-
- /* The following function should preferably be called with interrupts enabled.
- It preserves the state of the interrupt flag, and explicitly disables
- interrupts at several places, including disabling interrupts for up to 1.5
- ms during the read operation. It returns -1 if an error occurred (i.e. bad
- input number specified, or timeout), otherwise it returns the number of CTC
- clocks measured. It reads a single joystick pot input. */
-
- unsigned int ctc2_read_joystick(unsigned int inputnum) {
- unsigned char joymask; /* Bitmask for input */
- unsigned int endtime; /* Count in timer 2 at end of pulse */
- if (inputnum > 3)
- return -1; /* Invalid input number */
- joymask = 1 << inputnum;
- asm pushf;
- asm cli;
- outportb(PORTB, (inportb(PORTB) & 0xFC) | 0x01); /* Enable Timer 2 */
- asm popf;
- if (inportb(JOYPORT) & joymask) { /* Check for still timing out */
- asm pushf;
- asm cli;
- outportb(0x43, 0xB0); /* Chan. 2, two-byte, mode 0 */
- outportb(0x42, MAXCTCCLOCKS & 0xFF);
- outportb(0x42, MAXCTCCLOCKS >> 8);
- while (inportb(JOYPORT) & joymask) {
- if (inportb(T2PORT) & T2OUT) {
- asm popf;
- return -1;
- }
- }
- asm popf;
- }
- asm jmp SHORT $+2
- asm jmp SHORT $+2 /* Sniff for pending interrupts */
- asm pushf;
- asm cli;
- outportb(0x43, 0x90); /* Channel 2, lobyte-only, mode 0 */
- outportb(0x42, WAITCTCCLOCKS);
- while ((inportb(T2PORT) & T2OUT) == 0)
- ; /* Wait for a short time */
- asm popf;
- asm jmp SHORT $+2
- asm jmp SHORT $+2 /* Sniff for pending interrupts */
- asm pushf;
- asm cli;
- outportb(0x43, 0xB0); /* Chan. 2, two-byte, mode 0 */
- outportb(0x42, MAXCTCCLOCKS & 0xFF);
- outportb(0x42, MAXCTCCLOCKS >> 8); /* Start channel 2 */
- outportb(JOYPORT, 0); /* Start monostables */
- while (inportb(JOYPORT) & joymask) {
- if (inportb(T2PORT) & T2OUT) { /* Timed out */
- asm popf;
- return -1;
- }
- }
- outportb(0x43, 0x80); /* Latch timer 2 */
- endtime = inportb(0x42);
- endtime += inportb(0x42) << 8;
- asm popf;
- return MAXCTCCLOCKS - endtime;
- }
-
- /* The following function should be called with interrupts enabled. It will
- lock out interrupts for up to about 1.5 ms during the main timing cycle.
- It reads all four joystick positions. */
-
- void refd_read_joystick(joyvals * jv) {
- unsigned char counts[16]; /* Counts per input combination */
- unsigned char refcount; /* Counter for refreshes */
- register unsigned char portbval; /* Value from port B, and counter */
- register unsigned char inlast, inthis; /* Joystick port input values */
- unsigned char timedout; /* Inputs that timed out either phase */
- unsigned char changed; /* Inputs that changed */
- /* Check for any monostables still timing out */
- portbval = inportb(PORTB);
- for (refcount = 1; refcount < MAXREFRESH; ++refcount) {
- inthis = inportb(JOYPORT) & MONOS;
- if (!inthis)
- break; /* All monostables finished */
- while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
- ;
- portbval ^= 0xFF;
- }
- timedout = inthis; /* Set bits for inputs that timed out */
- /* Initialise counts and wait sixteen refreshes for monostables to stabilise */
- for (inthis = 0; inthis < 16; ++inthis) {
- counts[inthis] = 0;
- while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
- ;
- portbval ^= 0xFF;
- }
- inlast = MONOS; /* Initialise most recent input value */
- /* Timing critical stuff - could be optimised to assembly language */
- asm pushf;
- asm cli; /* Lock ints for timing critical stuff */
- portbval = inportb(PORTB);
- while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
- ; /* Wait for refresh detect to change */
- portbval ^= 0xFF;
- outportb(JOYPORT, 0); /* Start the monostables */
- for (refcount = 1; refcount < MAXREFRESH; ++refcount) {
- inthis = inportb(JOYPORT) & MONOS;
- if (inthis < inlast)
- counts[inlast = inthis] = refcount;
- if (!inthis)
- break; /* All monostables finished */
- while (((inportb(PORTB) ^ portbval) & REFDET) == 0)
- ; /* Wait for it to change */
- portbval ^= 0xFF;
- }
- asm popf;
- timedout |= inthis; /* Any that timed out this time */
- /* Now figure out what happened */
- jv->ax = jv->ay = jv->bx = jv->by = -1;
- inlast = 0;
- for (inthis = 0; inthis <= MONOS; ++inthis) {
- if ((refcount = counts[MONOS - inthis]) != 0) {
- changed = (inthis - inlast) & (timedout ^ 0xFF);
- inlast = inthis;
- if (changed & 1)
- jv->ax = refcount;
- if (changed & 2)
- jv->ay = refcount;
- if (changed & 4)
- jv->bx = refcount;
- if (changed & 8)
- jv->by = refcount;
- }
- }
- return;
- }
-
- void bios_read_joystick(joyvals * jv) {
- unsigned int jax, jay, jbx, jby;
- _AX = 0x8400;
- _DX = 0x0001;
- geninterrupt(0x15);
- jax = _AX;
- jay = _BX;
- jbx = _CX;
- jby = _DX;
- jv->ax = jax;
- jv->ay = jay;
- jv->bx = jbx;
- jv->by = jby;
- return;
- }
-
- void main(void) {
- joyvals refdvals, biosvals;
- printf("Sample program #18 - Demonstrates reading joystick positions\n"
- "Part of the PC Timing FAQ / Application notes\n"
- "By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"
- "Timeout (input not connected) is indicated by 65535 for the CTC2\n"
- "\tand RefDet methods, and 00000 for the BIOS function method\n\n"
- "Press <Esc> to exit\n\n"
- "----- CTC2 method ----- ---- RefDet method ----"
- " ----- BIOS method -----\n\n");
-
- while (1) {
- refd_read_joystick(&refdvals);
- bios_read_joystick(&biosvals);
- printf("%05u,%05u,%05u,%05u %05u,%05u,%05u,%05u %05u,%05u,%05u,%05u\r",
- ctc2_read_joystick(0), ctc2_read_joystick(1),
- ctc2_read_joystick(2), ctc2_read_joystick(3),
- refdvals.ax, refdvals.ay, refdvals.bx, refdvals.by,
- biosvals.ax, biosvals.ay, biosvals.bx, biosvals.by);
- if (bioskey(1))
- if ((bioskey(0) & 0xFF) == 27)
- break;
- }
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- The logic of ctc2_read_joystick() is not obvious so I will explain.
- The function only measures one joystick input, and it may have been called
- recently, so the input it is about to measure may still be timing out from an
- earlier call to ctc2_read_joystick(). The function tests explicitly for this,
- and if this is the case, it performs a timeout detection in the first while()
- loop, waiting for the monostable output to go low. If the monostable output
- does not go low within the timeout period, the function returns -1.
-
- If the monostable output is, or already was, low, then a short delay of about
- 16 CTC clocks plus overhead is inserted, to give a minimum recovery time for
- the monostable circuitry which must discharge or recharge the capacitor fully.
- If the monostable is triggered too quickly after it has timed out, the capacitor
- might not be fully discharged or recharged, resulting in an unusually short
- pulse, because the capacitor doesn't have to charge or discharge so far to reach
- the monostable threshold.
-
- Then, CTC channel 2 is programmed with a count of MAXCTCCLOCKS and the joystick
- monostables are triggered. This section of code operates with interrupts
- locked out. It continually checks the joystick status, and checks whether a
- timeout has occurred. A timeout is indicated by the Timer 2 Output signal on
- the I/O port at I/O address 61h (62h on the PC and XT). If a timeout occurs,
- the function returns -1. If the monostable times out and its status line goes
- low within the timeout period, the count in CTC channel 2 is latched, and the
- number of elapsed CTC clocks is calculated and returned. The function will
- always return within about 2 x MAXCTCCLOCKS CTC clocks (units of 0.838 us) plus
- interrupt overhead, unless the CTC is faulty.
-
- See section »» 7.30 for a detailed explanation of the timing method using CTC
- channel 2 in this way.
-
- The logic of refd_read_joystick is similar, but it watches for transitions on
- the Refresh Detect signal to measure elapsed time. Whenever the monostable
- bits in the joystick port value change, the counts[] array is updated with the
- Refresh Detect count for the appropriate input pattern. This means that if
- more than one monostable times out within the same sample period, the code does
- not have to potentially update up to four variables, possibly missing a Refresh
- Detect transition. The four returned values are calculated after the timing
- critical section has completed. The code also keeps flags for inputs which have
- timed out, either in the initial checking phase, or the main timing phase, and
- always returns -1 for these inputs.
-
- ## 10.4.5 USING THE JOYSTICK PORT FOR GENERAL PURPOSE INPUT
-
- The joystick button inputs can be used as general purpose button or switch
- inputs, and can also be driven by logic level signals or by open collector or
- open drain logic outputs. If used with a signal direct from a mechanical
- contact (e.g. a switch, microswitch, contact, or pushbutton), remember that
- the joystick port does not perform hardware debouncing, so this must be
- provided by external hardware or provided by software.
-
- Provided that you can tolerate poor accuracy, poor repeatability, poor matching
- between channels, and poor temperature stability, you can use the joystick
- position inputs as general purpose analogue inputs, but don't fart too close
- to them. The inputs should not be voltage-driven, they should be driven from
- a variable resistor from a positive supply rail such as the 5V rail (the way
- the joystick itself works), or from a positive variable current source. This
- gives a roughly linear relationship between resistance and time measured, which
- means an inverse (reciprocal) relationship between current and time measured.
-
- A voltage signal can be converted into a variable current signal, and a circuit
- to do this is given in Figure 5 in the FIGURES archive. This circuit converts
- a positive, ground-referenced voltage into a positive current source that can
- be fed into one joystick position input. The relationship between input voltage
- and output current is linear. 1V on the input produces an output current of
- 1mA. The circuit requires a 9-12V supply, which is unfortunately not available
- on the joystick port, though you could use a switched capacitor voltage booster
- (e.g. the Linear Technology LT1054) or a switching supply (e.g. the Motorola
- MC34063 or the National Semiconductor LM2574 series) to produce a higher voltage
- rail from the 5V output on the joystick port, but be aware that switching power
- supplies can create a lot of electrical noise.
-
- Because the relationship between input voltage and time measured is reciprocal,
- a zero input voltage will give an infinite timeout. Obviously this should be
- avoided, as it will prevent software from reading the inputs within a reasonable
- period of time. This can be prevented by ensuring that the input voltage never
- falls below a certain threshold, or it could be prevented by incorporating an
- offset in the voltage to current converter. In the very unlikely event that
- you are interested in pursuing this, I may be able to help so please drop me
- an email message.
-
- ## 10.4.6 JOYSTICK LEFT/RIGHT AND UP/DOWN DETECTION
-
- If you simply want to detect whether the joystick is left or right of centre,
- or above or below centre, and don't want the overhead of locking interrupts
- for several milliseconds at regular intervals, you could use a fast tick
- interrupt to poll the joystick port. I would suggest using an interrupt at
- about 500 us and working cyclically through three states. On one interrupt,
- trigger the joysticks. On the next interrupt, read the monostable states.
- On the next interrupt, do nothing. On the next interrupt, you're back to the
- first interrupt again, so trigger the monostables again. This will give a
- left/right and up/down indication every 1.5 ms, with a fairly low overhead.
-
- ## 10.5 THE MOUSE AND MOUSE DRIVER [NOT WRITTEN]
-
- I haven't investigated the mouse or the mouse driver. The format of the serial
- data is documented (see {JAM}'s documents for the basic information) but I have
- nothing on its use of the timer tick interrupt or the CTC hardware. This
- section may (or may not :-) be completed at a later date. Any information is
- welcomed. (*)
-
- ## 10.6 NETWORKS
-
- I have no experience with networks, so I will quote (paraphrased) from {JAM}'s
- documents (see section »» 1.7).
-
- The int 8 overhead is increased when network software is installed, because the
- network software uses the interrupt to check whether the network is still
- functioning properly. This increase is not really significant. Details are
- documented in the Netware book (see the references section). However, the
- network card interrupts the processor via the network card's own interrupt,
- whenever the processor must process and respond to a data packet. This occurs
- even if the computer is not using the network at the time, because the network
- still checks regularly that the computer is present. {JAM} continues: "Other
- machines were checked with just Pathworks or just Novell and the errors are
- similar to this. In fact, for machines using Novell over broadband networks,
- delays in the order of 1.5 to 2 milliseconds were not uncommon. The actual
- numbers presented here should be taken with a grain of salt; they are going to
- differ widely with different networks, loads, CPU speeds, and network cards".
-
- ## 10.7 SOUND GENERATION
-
- Though the PWM method of sound generation is widely used, the specific method
- of generating it on a PC, described in this section and subsections, was (to
- my knowledge) first described Mark Feldman the PC-GPE (PC Games Programmer's
- Encyclopedia) guru (see section »» 1.7), and subsequently developed by Peter
- Moylan and Tim Channon (see section »» 1.7 and »» 10.7.4). It has probably
- been independently developed by others. The documentation and the coding of
- the sample program are my own.
-
- The PC's basic beep sound makes the speaker cone move between two positions -
- in and out. This is shown by the following 'waveform' which graphs speaker
- position (on the vertical axis) against time (horizontal axis).
-
- IN ┌───────────────┐ ┌───────────────┐
- CONE │ │ │ │
- OUT ───┘ └───────────────┘ └─────
- 1 2 3 4 5 6 7 8 ms
- TIME...
-
- This is digital (on or off) control, and this level of control severely limits
- the type and subtlety of the sounds that can be generated. Better sound
- requires the speaker to be put in more than two positions. For example, an
- 8-bit sound card such as a Sound Blaster gives 256 discrete output voltages or
- speaker positions, using an analogue signal which can assume any of 256
- discrete values, and CDs and good quality sound cards use a 16-bit converter
- that gives 65536 discrete values.
-
- A digital control can approximate this to a limited degree using a technique
- called Pulse Width Modulation (PWM), where a digital signal made up of pulses
- at a high frequency is _averaged_ by the hardware. The width of the pulses is
- adjusted ('modulated') and this varies the _average_ voltage of the signal when
- it is averaged over a short period of time. If the pulse rate is high enough,
- the speaker will not be able to follow the pulses themselves, but will follow
- the average value. If the pulse widths, and therefore the average value, are
- varied at audio frequency, the average value, and therefore the speaker cone
- position, varies at audio frequency, and audible sound is generated.
-
- ## 10.7.1 PULSE WIDTH MODULATION (PWM) PRINCIPLE
-
- 5V ┌─┐ ┌─┐ ┌─┐
- │ │ │ │ │ │ 25% duty cycle
- 0V ─┘ └─────┘ └─────┘ └──── Average voltage = 1.25V
-
- 5V ┌───┐ ┌───┐ ┌───┐
- │ │ │ │ │ │ 50% duty cycle
- 0V ─┘ └───┘ └───┘ └── Average voltage = 2.5V
-
- 5V ┌─────┐ ┌─────┐ ┌─────┐
- │ │ │ │ │ │ 75% duty cycle
- 0V ─┘ └─┘ └─┘ └ Average voltage = 3.75V
-
- Simple PWM (shown above) uses a fixed pulse rate, and varies the pulse width.
- Notice that the rising edges on the above waveforms are all in sync and regular.
-
- The diagram below shows a PWM pulse stream, with pulse start points marked, and
- the corresponding approximate average value, showing the audio content in the
- signal. If the pulse rate is high enough, only the audio component is audible.
-
- ┌───┐ ┌─────┐ ┌─────┐ ┌───┐ ┌─┐ ┌─┐ ┌───┐ ┌─────┐ ┌─
- PWM │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
- ─┘ └───┘ └─┘ └─┘ └───┘ └─────┘ └─────┘ └───┘ └─┘
- ^ ^ ^ ^ ^ ^ ^ ^
-
- ──────────────── ────────
- AVERAGE ──────── ──────── ────────
- ────────────────
-
- Figure 6 in the FIGURES archive shows this a bit more clearly.
-
- ## 10.7.2 PWM AUDIO GENERATION IMPLEMENTATION
-
- PWM audio generation can be done directly by the microprocessor, but this is
- unreliable due to memory caching and other factors that may affect the speed
- of the processor's operation. The generic method uses CTC channels zero and
- two, and gives more consistent operation. Channel zero is used to generate
- interrupts at the pulse rate, typically 11kHz or higher, and the int 8 handler
- uses channel two to generate the pulses.
-
- ## 10.7.3 SAMPLE PROGRAM: DTMF GENERATION USING PWM
-
- The following sample program uses PWM to generate DTMF (dual tone multiple
- frequency) tones, also known as touch tones, which are used for signalling
- numbers being dialled on a touch tone telephone.
-
- The audio output from the program is very quiet, so I have not been able to
- confirm that it will actually dial a telephone, but its main purpose is to
- present the techniques and sample code.
-
- This program takes over the timer tick interrupt, operating it at about 18000
- interrupts (PWM pulses) per second. It does not chain to the BIOS handler, and
- it does not restore the correct DOS time from the RTC on termination. This
- program will cause loss of time when run. The time can be corrected by
- rebooting the machine.
-
- -------------------------------- snip snip snip --------------------------------
- NAME SAMPLE19
-
- ; Sample program #19
- ; Demonstrates DTMF (touch tone) generation using PWM sound techniques
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This program assembles into SAMPLE19.COM, a small command-line driven program
- ; which generates DTMF (dual tone multiple frequency) tones, also known as
- ; touch tones, using PWM sound techniques through the PC speaker, according to
- ; the command line parameters.
- ;
- ; Save this file to SAMPLE19.ASM and assemble with:
- ; masm SAMPLE19;
- ; link SAMPLE19;
- ; exe2bin SAMPLE19.exe SAMPLE19.com
- ; or
- ; tasm SAMPLE19;
- ; tlink /t SAMPLE19;
- ;
- ;
- ; Note - this program will _not_ run properly under OS/2, Linux, Windows, or
- ; anything other than plain DOS. If possible, it should be run without EMM386
- ; or QEMM or any other memory manager, particularly on slower machines such as
- ; 386SX or slow 386 machines.
-
- PulseDivisor = 66 ; Interrupt rate is 1.1931816666... MHz
- ; divided by this value
- Fifty = PulseDivisor/2 ; Pulse width for roughly 50% duty
-
- ; The chosen PulseDivisor of 66 gives an interrupt rate of about 18,079
- ; interrupts per second.
-
- ; The following GW-BASIC program generates the 256-entry sinewave table with
- ; maximum spans of +/- 16, centre zero, using signed values. The span must
- ; be chosen so that when two sinewaves are added together and added to the
- ; 'Fifty' value (which represents half the number of CTC clocks between PWM
- ; pulses), the range of possible pulse widths is within the tolerance of the
- ; PWM interrupt rate. In this case, the maximum excursion for two summed
- ; sinewaves is taken to be +/- 32 (two sinewaves, each at +/- 16). When added
- ; to the 'Fifty' value (33), the pulse width range is 1 to 65.
- ; If you change the pulse rate, you must change the span (the 16# in line 20)
- ; appropriately. I chose a span of roughly (PulseDivisor - 2) / 4.
- ;
- ; 10 OPEN "SINE.DMP" FOR OUTPUT AS#1 : A# = 0 : I# = 3.141592653589793#/128#
- ; 20 FOR P = 0 TO 255 : S# = SIN(A#) : V = INT((S# * 16#) + .5#) : PRINT #1,V
- ; 30 A# = A# + I# : NEXT : CLOSE #1 : SYSTEM
- ;
- ; I used the following GW-BASIC program to generate lists of delta sequences
- ; for indexing into the 256-entry sinewave table. The A#=... value in line 20
- ; specifies the sample rate; the last number in the equation is PulseDivisor.
- ; If you change PulseDivisor, modify this and rerun the program.
- ; To use the program, input the desired frequency, and it will calculate
- ; possible delta sequences and prompt you. Initially, just press Enter at
- ; the prompt, until you have chosen the delta sequence you will use. Then
- ; break and rerun the program, and at the prompt for the chosen sequence,
- ; type 'y <enter>'. The program will append to a file '$CALC.DMP' and list
- ; the delta sequence.
- ;
- ; 10 REM $CALC - Calculation for DTMF generator program
- ; 20 A#=14318180#/12#/66# : INPUT F : PRINT "There are" A#/F "samples/cycle"
- ; 30 I# = 256 * F / A# : PRINT "Samples are spaced at intervals of" I#
- ; 40 X# = 0 : D = 1 : R = 0
- ; 50 R = R + 1 : X# = X# + I# : Z = ABS(X# - INT(X# + .5#)) : IF Z >= D THEN 50
- ; 60 PRINT R "samples, error is" Z;: D = Z : INPUT Q$ : IF Q$ <> "y" GOTO 50
- ; 70 OPEN "$CALC.DMP" FOR APPEND AS#1 : PRINT#1, F ":" R "values" : S# = 0
- ; 80 I = 0 : FOR P = 1 TO R : SD = -I : S# = S# + I# : I = INT(S# + .5#)
- ; 90 SD = SD + I : PRINT#1, SD;: NEXT : PRINT#1,"" : CLOSE #1 : END
-
- Code SEGMENT
- ASSUME cs:Code,ds:Code,es:nothing,ss:nothing
-
- ORG 100h
- Begin: jmp Begin2 ; Skip data
-
- SignOnMsg DB "Sample program #19 - DTMF generator demonstrating PWM sound generation",13,10
- DB "Part of the PC Timing FAQ / Application notes",13,10
- DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10
- DB "Characters 0-9, *, #, and A-D generate DTMF pairs",13,10
- DB "Characters a-h generate single tones",13,10
- DB "A comma generates a 1/2-second pause",13,10,"$"
-
- ALIGN 2
-
- ParmPointer DW 81h ; Pointer into command tail
- OldInt8Ofs DW 0 ; Old int 8 handler offset
- OldInt8Seg DW 0 ; Old int 8 handler segment
- PWMBufferGet DW 0 ; 'Get' offset for PWMBuffer (volatile)
- PWMBufferPut DW 0 ; 'Put' offset for PWMBuffer
- PIC0IMR DB 0 ; PIC IMR before we stopped all but IRQ0
-
- ALIGN 2
-
- Tone0Ctrl DW 0,0 ; Delta and sinewave table pointers
- Tone1Ctrl DW 0,0 ; Same, for other tone of the pair
-
- CharScanTable: DW "0",Row4,Col2
- DW "1",Row1,Col1
- DW "2",Row1,Col2
- DW "3",Row1,Col3
- DW "4",Row2,Col1
- DW "5",Row2,Col2
- DW "6",Row2,Col3
- DW "7",Row3,Col1
- DW "8",Row3,Col2
- DW "9",Row3,Col3
- DW "*",Row4,Col1
- DW "#",Row4,Col3
- DW "A",Row1,Col4
- DW "B",Row2,Col4
- DW "C",Row3,Col4
- DW "D",Row4,Col4
- DW "a",Row1,0
- DW "b",Row2,0
- DW "c",Row3,0
- DW "d",Row4,0
- DW "e",Col1,0
- DW "f",Col2,0
- DW "g",Col3,0
- DW "h",Col4,0
- PastCharTable = $
-
- Row1 DW 10,10,10,9,10,10,10,10,Row1 ; 697 Hz
- Row2 DW 11,11,11,11,11,10,11,11,11,11,Row2 ; 770 Hz
- Row3 DW 12,12,12,12,12,12,12,13,12,12,12,12,12,12,12,Row3 ; 852 Hz
- Row4 DW 13,14,13,Row4 ; 941 Hz
- Col1 DW 17,17,17,17,18,17,17,17,Col1 ; 1209 Hz
- Col2 DW 19,19,19,19,19,19,18,19,19,19,19,19,Col2 ; 1336 Hz
- Col3 DW 21,21,21,21,21,20,21,21,21,21,21,21,Col3 ; 1477 Hz
- Col4 DW 23,23,23,23,24,23,23,23,Col4 ; 1633 Hz
-
- SineTable DB 0,0,1,1,2,2,2,3,3,4,4,4,5,5,5,6,6,6,7,7,8,8,8,9,9,9,10
- DB 10,10,10,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14
- DB 14,14,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16
- DB 16,16,16,16,16,16,16,16,16,16,16,15,15,15,15,15,15,15,14
- DB 14,14,14,14,14,13,13,13,13,12,12,12,12,11,11,11,10,10,10
- DB 10,9,9,9,8,8,8,7,7,6,6,6,5,5,5,4,4,4,3,3,2,2,2,1,1,0,0,0
- DB -1,-1,-2,-2,-2,-3,-3,-4,-4,-4,-5,-5,-5,-6,-6,-6,-7,-7,-8
- DB -8,-8,-9,-9,-9,-10,-10,-10,-10,-11,-11,-11,-12,-12,-12
- DB -12,-13,-13,-13,-13,-14,-14,-14,-14,-14,-14,-15,-15,-15
- DB -15,-15,-15,-15,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16
- DB -16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15,-15,-15
- DB -15,-15,-15,-15,-14,-14,-14,-14,-14,-14,-13,-13,-13,-13
- DB -12,-12,-12,-12,-11,-11,-11,-10,-10,-10,-10,-9,-9,-9,-8
- DB -8,-8,-7,-7,-6,-6,-6,-5,-5,-5,-4,-4,-4,-3,-3,-2,-2,-2,-1
- DB -1,0
-
- ALIGN 4 ; No need to do this, really.
-
- PWMBuffer DB 256 DUP(?) ; PWM width-value data buffer (circular)
-
- Begin2: mov dx,OFFSET SignOnMsg ; Point to sign-on message
- mov ah,9 ; Function number
- int 21h ; Output the message
- cld ; Upwards direction
- call InitialisePWM ; Initialise and start PWM stuff
- DigitLoop: mov bx,ParmPointer ; Get pointer into command tail
- inc ParmPointer ; Bump it
- mov al,[bx] ; Get character from command tail
- cmp al,13 ; End of command tail?
- je DigitsDone ; If so
- cmp al," " ; Whitespace?
- jbe DigitLoop ; If so, skip it
- cmp al,"," ; Comma?
- jne NotComma ; If not
- mov cx,7850 ; If so, pause for half a second
- call MakeDelay
- jmp SHORT DigitLoop ; Loop
- NotComma: mov bx,OFFSET CharScanTable-6 ; Point to before first entry
- NextCharScan: add bx,6 ; Point to next
- cmp bx,OFFSET PastCharTable ; Scanned whole table?
- jae DigitLoop ; If not found, skip it
- cmp al,[bx] ; Check for match
- jne NextCharScan ; If not, loop
- mov ax,[bx+2] ; Get first tone pointer
- mov dx,[bx+4] ; Get second tone pointer
- mov cx,3140 ; 200 ms duration
- call MakeDTMF
- mov cx,785 ; 50m ms pause
- call MakeDelay
- jmp SHORT DigitLoop ; Loop
-
- DigitsDone: call UninstallPWM
- mov ax,4C00h
- int 21h
- int 20h
-
- MakeDTMF PROC near
- mov Tone0Ctrl,ax
- mov Tone0Ctrl+2,0
- mov Tone1Ctrl,dx
- mov Tone1Ctrl+2,0
- DTMFLoop: mov bx,OFFSET Tone0Ctrl
- call GetPulseWidth
- mov bx,OFFSET Tone1Ctrl
- cmp WORD PTR [bx],0
- jz SingleTone
- xchg ax,dx
- call GetPulseWidth
- add al,dl
- SingleTone: add al,Fifty
- call PutPWM
- loop DTMFLoop
- ret
- MakeDTMF ENDP
-
- MakeDelay PROC near
- DelayLoop: mov al,Fifty
- call PutPWM
- loop DelayLoop
- ret
- MakeDelay ENDP
-
- GetPulseWidth PROC near ; Call with BX pointing to first or
- cld ; second tone control structure
- mov si,[bx] ; Get delta table pointer
- lodsw ; Get a delta or pointer
- test ah,ah ; Was it a pointer?
- jz GotDelta ; If not
- mov si,ax ; If pointer, reset pointer
- lodsw ; Get it, and increment pointer
- GotDelta: mov [bx],si ; Return the delta table pointer
- mov si,[bx+2] ; Get sine table pointer
- add si,ax ; Add delta
- and si,0FFh ; Wrap around
- mov [bx+2],si ; Restore sine table pointer
- mov al,SineTable[si] ; Get sine table entry
- ret
- GetPulseWidth ENDP
-
- InitialisePWM PROC near
-
- ; Get the current int 8 handler address, to be restored later
-
- mov ax,3508h ; Get interrupt vector for int 8
- int 21h ; Call DOS
- mov OldInt8Ofs,bx ; Store offset
- mov OldInt8Seg,es ; Store segment
-
- ; Initialise PWM array with 50% duty cycle entries
-
- xor bx,bx ; Zero offset
- mov al,Fifty ; 50% duty cycle
- FillPWMBuf: mov PWMBuffer[bx],al ; Set entry
- inc bl ; Bump offset
- jnz FillPWMBuf ; Do all entries
-
- ; Wait for all floppy drives to turn off
-
- xor ax,ax ; Zero
- mov es,ax ; Address BIOS data area with ES
- WaitMotors: test BYTE PTR es:[43Fh],0Fh ; Any floppy drive motors active?
- jnz WaitMotors ; If so, wait
-
- ; Disable all interrupt sources on the primary (or only) PIC. Keep the
- ; original IMR contents, to be restored later.
-
- cli
- in al,21h ; Read primary PIC IMR
- jmp SHORT $+2 ; Short delay
- mov PIC0IMR,al ; Store it for later
- mov al,0FFh ; Mask off _everything_
- out 21h,al
-
- ; Set up Port B and CTC channel 2
-
- in al,61h ; Read Port B
- jmp SHORT $+2 ; Short delay
- and al,11111101b ; Speaker enable OFF
- or al,00000001b ; Timer 2 gate ON
- out 61h,al ; Write it back
- jmp SHORT $+2 ; Short delay
- mov al,10010000b ; Channel 2, lobyte access, mode 0
- out 43h,al ; Set mode of channel 2 (no values yet)
- sti ; Allow interrupts
-
- ; Now grab int 8, the timer tick interrupt. DOS should leave the PIC IMR alone.
-
- mov dx,OFFSET NewInt8 ; Offset of new int 8 routine
- mov ax,2508h ; Set interrupt vector for int 8
- int 21h ; Call DOS to do it
-
- ; Reprogram CTC channel 0 with the new interrupt rate
-
- cli ; Lock out interrupts again
- mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3
- out 43h,al ; Write mode/command register
- jmp SHORT $+2 ; Short delay
- mov al,LOW PulseDivisor ; Lobyte of new divisor
- out 40h,al ; Send it
- jmp SHORT $+2 ; Short delay
- mov al,HIGH PulseDivisor ; Hibyte of new divisor
- out 40h,al ; Send it
- jmp SHORT $+2 ; Short delay
-
- ; Enable the speaker
-
- in al,61h
- jmp SHORT $+2 ; Short delay
- or al,00000011b ; Timer 2 gate and speaker enable ON.
- out 61h,al
- jmp SHORT $+2 ; Short delay
-
- ; Enable int 8 (IRQ0) in the PIC
-
- mov al,11111110b ; All masked except IRQ0
- out 21h,al ; Set primary PIC IMR
- jmp SHORT $+2 ; Short delay
-
- ; Start interrupts and return to caller
-
- sti ; Tag! You're it :-)
- nop
- mov al,BYTE PTR PWMBufferGet ; Get 'get' pointer
- dec ax ; Back up one
- mov BYTE PTR PWMBufferPut,al ; Output one bufferful
- ret
- InitialisePWM ENDP
-
- UninstallPWM PROC near
-
- ; Disable the speaker
-
- pushf ; Preserve interrupt flag
- cli ; Lock out interrupts around this
- in al,61h ; Read Port B
- jmp SHORT $+2 ; Short delay
- and al,11111100b ; Disable Timer 2 and speaker
- out 61h,al ; Write it back
- jmp SHORT $+2 ; Short delay
-
- ; Disable all interrupt sources in the primary PIC
-
- mov al,0FFh ; Mask all IRQ0-7
- out 21h,al ; Set PIC0 IMR
-
- ; Restore normal operation of CTC channel 0
-
- mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3
- out 43h,al ; Write mode/command word
- jmp SHORT $+2 ; Short delay
- xor al,al ; Zero
- out 40h,al ; Write loword of reload value
- jmp SHORT $+2 ; Short delay
- out 40h,al ; Write hiword of reload value
- popf ; Interrupts are safe (PIC is blocked)
-
- ; Restore original int 8 handler address
-
- push ds ; Will need to destroy DS for this
- mov dx,OldInt8Ofs ; Get offset
- mov ds,OldInt8Seg ; Get segment
- ASSUME ds:nothing ; DS no longer points to this segment
- mov ax,2508h ; Set int 8 vector
- int 21h ; Call DOS
- pop ds ; Restore DS
-
- ; Restore original IMR
-
- mov al,PIC0IMR ; Get old IMR contents
- out 21h,al ; Restore IMR
- ret
- UninstallPWM ENDP
-
- ; The following function stuffs a pulse width value into the circular buffer,
- ; first waiting for the interrupt routine's outgoing data pointer to catch up,
- ; if necessary. This prevents the foreground code from generating data more
- ; quickly than the interrupt routine is taking it, and maintains synchronisation
- ; between the two processes, unless the foreground code generates the data too
- ; slowly.
-
- PutPWM PROC near ; Put width in AL into buffer
- mov bx,PWMBufferPut ; Get the 'put' offset
- mov PWMBuffer[bx],al ; Store the width value in buffer
- inc bl ; Bump 'put' pointer
- mov PWMBufferPut,bx ; Store it back
- WaitBufFull: cmp bl,BYTE PTR PWMBufferGet ; If buffer is full...
- je WaitBufFull ; ... wait until there's a gap
- ret
- PutPWM ENDP
-
- ASSUME ds:nothing
-
- ; This program uses CTC channel 0 as a timebase, generating int 8 at regular
- ; intervals, and CTC channel 2 producing variable width pulses. The interrupt
- ; routine programs an 8-bit count value into channel 2 on every invocation, and
- ; channel 2 produces a pulse of the corresponding length on the speaker output
- ; signal. Because the interrupt rate is constant and the pulse width varies,
- ; pulse width modulation (PWM) sound is generated.
- ; This is the int 8 handler. It gets data from PWMBuffer, using PWMBufferGet
- ; as an offset into PWMBuffer indicating where it's currently up to. It bumps
- ; this variable on each timer interrupt. The bump increments the lobyte only,
- ; so that the offset wraps around from 255 back to 0 again (the buffer is 256
- ; bytes in size). This code does not check to see whether PWMBufferGet has
- ; been bumped past PWMBufferPut. The foreground code must be fast enough to
- ; keep the buffer full - if not, the int 8 processing will repeat-play the
- ; buffer.
- ; Each entry in the buffer is one byte, and corresponds to the pulse width of
- ; one pulse. The data in this buffer is generated by the foreground code.
- ; The following code could be optimised somewhat - by page-aligning the
- ; PWM buffer, the MOV AL,PWMBuffer[BX] could be replaced with a direct load
- ; using a self-modified pointer, also removing the need to preserve BX.
-
- NewInt8 PROC far
- push bx ; Preserve
- push ax ; Preserve
- mov bx,PWMBufferGet ; Get pointer to data coming from buffer
- mov al,PWMBuffer[bx] ; Get one pulse-width byte from buffer
- out 42h,al ; Tell CTC channel 2 to make a pulse
- inc BYTE PTR PWMBufferGet ; Bump pointer (256-byte buffer)
- mov al,20h ; EOI command
- out 20h,al ; Send to primary PIC
- pop ax ; Restore
- pop bx ; Restore
- iret ; Return from interrupt
- NewInt8 ENDP
-
- Code ENDS
- END Begin
- -------------------------------- snip snip snip --------------------------------
-
- ## 10.7.3.1 SAMPLE PROGRAM EXPLANATION
-
- Channel 0 is operated in mode 2 or 3, and generates interrupts (int 8) at
- regular intervals. Each int 8 will trigger the start of one pulse. The int
- 8 handler, NewInt8, will output a pulse-width value to the CTC channel 2 data
- register, and CTC channel 2 will produce a pulse of the corresponding length.
-
- Channel 2 is operated in mode 0, known as 'interrupt on terminal count' mode
- (see section »» 7.8.2). When CTC channel 2 has been initialised for this mode,
- and the Timer 2 Gate output in the Port B register (see section »» 7.5) is set
- to enable clocking of CTC channel 2, writing a count value to the channel 2
- reload register will cause the CTC channel 2 output to go low for a period of
- time determined by the value written to the channel 2 register. By controlling
- the values written to the channel 2 register, the pulse width can be varied.
-
- The pulse width will be the CTC clock period (0.838 us) multiplied by the value
- written to the channel 2 register. To improve efficiency, because pulses are
- typically much less than 256 CTC clocks wide, CTC channel 2 is configured in
- lobyte-only access mode. Only one I/O access, to write a byte of data to CTC
- channel 2, is required to trigger a pulse on CTC channel 2. If the Speaker
- Data bit in Port B is set, the pulse will be sent to the PC's speaker.
-
- The above description covers the PWM output code, which consists of an interrupt
- handler, triggered at regular intervals via int 8 from CTC channel 0. The
- handler writes an 8-bit pulse-width value to the CTC channel 2 data register.
-
- The data that it writes is taken from a circular buffer. The interrupt handler
- maintains a pointer, the 'get' pointer, called PWMBufferGet, which lets it keep
- track of where it is up to in the buffer. On every interrupt, it loads the BX
- register from the 'get' pointer, reads a pulse-width value from the circular
- buffer at the appropriate position, and 'bumps' the 'get' pointer. The term
- 'bump' means to increment, but in this case, also wrap around from the end of
- the buffer to the start, as the buffer is circular.
-
- The actual data in the buffer is generated by the foreground code, and inserted
- into the buffer by the PutPWM function. This function maintains synchronisation
- between the foreground code and the interrupt handler, by checking that it will
- not overfill the buffer, before putting a byte into the buffer. This slows down
- the mainline code. As long as the mainline runs quickly enough, synchronisation
- between the mainline and the interrupt routine is maintained.
-
- The initialisation steps are fairly involved. All initialisation is done by
- the InitialisePWM function. First, the current int 8 handler address is stored
- so it can be restored later. Then the circular PWM buffer is filled with the
- 'Fifty' value, so that when the interrupt is started later, before the mainline
- has filled the buffer, it will play silence instead of garbage. The code then
- waits for all floppy drives to turn off. Because the replacement int 8 handler
- does not chain to the original handler, any actions normally done by the int 8
- handler, such as updating the BIOS timer tick count variable and turning off
- floppy drives after two seconds of inactivity, will not be performed during the
- execution of this program, so we must wait for the disk drives to turn off
- before replacing the int 8 handler, otherwise they will remain on during the
- program's execution.
-
- The initialisation code then disables all interrupt sources on the primary
- interrupt controller. IRQ0 (int 8), the timer tick interrupt, will be enabled
- shortly. The code then initialises Port B, initially with Speaker Enable off,
- and programs the operating mode (mode 0, interrupt on terminal count) and the
- access mod (lobyte-only) in CTC channel 2. It then redirects int 8 to its own
- int 8 handler, reprograms CTC channel 0 with the new interrupt rate (18,079
- interrupts per second), enables the Speaker Enable, and enables IRQ0 in the
- interrupt controller. Other interrupt sources are not enabled in the interrupt
- mask register of the primary PIC. This prevents interrupts due to a keypress
- from disturbing the sound generated. The system will not respond to keypresses
- while the program is running. It then enables interrupts, resets the 'put'
- pointer for the PWM buffer, and returns.
-
- Once this initialisation has been done, the interrupt routine will run quite
- happily in the background, outputting pulse widths from the circular buffer of
- pulse-width values. It will loop repeatedly through the buffer. Foreground
- code is required to set up the data in the buffer, and keep track of the 'get'
- pointer used by the interrupt routine, so it can control the flow of data into
- the circular buffer.
-
- The actual DTMF waveform generation is done via a 256-entry sinewave table with
- a span of +/- 16. The table contains one cycle of sinewave, and is indexed via
- a delta sequence table. Each of the eight possible frequencies has its own
- delta sequence table. The delta sequence table tells the program how many
- entries in the sinewave table to skip between PWM pulses (samples). For a high
- frequency, the deltas are large, so the program steps through the 256 entries
- of the sinewave table fairly quickly, and for lower frequencies, the delta is
- smaller. A table of deltas is required, to give the effect of a non-integral
- delta value so that reasonable frequency accuracy can be achieved.
-
- Running int 8 at these high rates causes a significant load on the machine,
- especially with slower machines. Using EMM386 adds interrupt overhead, and on
- slower machines, programs using this technique may not run properly with EMM386
- installed. I have done limited testing with the sample program, and found that
- it works properly on a 10MHz 286, but I can't guarantee its performance on, say,
- a 386SX-16 running EMM386, or on an XT.
-
- ## 10.7.3.2 OTHER METHODS OF SOUND GENERATION
-
- The same fast int 8 handler can be modified to output an 8-bit unsigned sample
- value to a parallel port, which is connected to a DAC (digital to analogue
- converter). This gives much better sound quality than the PWM technique.
-
- The digital to analogue converter can be a chip, such as the Ferranti ZN429
- or various devices from other manufacturers such as Analog Devices / PMI,
- Maxim, Burr-Brown, etc, or the el cheapo R-2R ladder DAC made from a chain of
- resistors. Commercial parallel port DAC units are available - the Speech
- Thing device is just a DAC on the parallel port.
-
- Sound cards have an 8-bit or 16-bit DAC, but are usually operated in DMA mode,
- where the sound card periodically requests an 8-bit or 16-bit data transfer
- from a buffer area in system memory and sends the value to the DAC. The DMA
- method gives much lower overhead, because the processor does not get involved
- in the transfer, and also removes the problem of sample timing jitter.
-
- ## 10.7.4 PETER MOYLAN'S MUSIC PACKAGE
-
- Peter Moylan's music package was written by Peter Moylan (see section »» 1.7)
- and Tim Channon. It uses CTC channel 0 with a divisor of 64, and CTC channel
- 2 in interrupt on terminal count mode (mode 0). It does not chain to the
- original int 8 handler, and does not fix up the DOS time on termination. This
- package produces 3-part polyphonic (i.e. three simultaneous pitches) music and
- supports several timbral qualities. It is noticeably out of tune, particularly
- at higher pitches, but this is due to limitations in the waveform generation
- algorithm, not the hardware technique used. Seven demonstration programs and
- source code in Modula-2 and assembler are included in the package, which is
- available on the Internet as: ftp://ee.newcastle.edu.au/pub/PMOS/music302.zip.
- The version number (3.02) may have changed.
-
- ## 10.8 RELATED SOFTWARE PACKAGES
-
- Here are my comments on some timing-related software packages available on the
- Internet. Many of these packages are several years old, so contact details may
- be well out of date. I have not checked any of the contact details.
-
- ## 10.8.1 THE ATIM PACKAGE
-
- ftp://oak.oakland.edu/SimTel/msdos/at/atim.zip
- Date: 19881125 Size: 4783
-
- This package contains a small program called ATIM which will run another
- program and time its execution, using the RTC periodic interrupt for timing
- (approximately one millisecond resolution). The program is written in
- assembler, and commented source and brief documentation is included. The
- package was written by Howard Vigorita, NYACC (whatever that means :-),
- December 27, 1986. I presume it is public domain, though he doesn't say so.
-
- The program seems to work quite nicely. I'm not sure about the algorithm he
- uses to convert 1/1024ths to 1/1000ths of seconds, though. The only problem
- I noticed was that "COMMAND.COM" was hardcoded as the command interpreter name
- if the program is assembled to use the DOS EXEC function instead of the back
- door execute function - after all, this program is nearly nine years old!
-
- ## 10.8.2 THE MSCHRT AND TCHRT PACKAGES
-
- ftp://oak.oakland.edu/SimTel/msdos/c/mschrt3.zip
- Date: 19910604 Size: 53708
- ftp://oak.oakland.edu/SimTel/msdos/turbo_c/tchrt3.zip
- Date: 19910605 Size: 53436
-
- MSCHRT and TCHRT version 3 are Microsoft C and Turbo C compatible versions of
- a "high resolution timer toolbox" distributed as a library, from Ryle Design,
- P.O. Box 22, Mt. Pleasant, Michigan 48804, (517) 773-0587, CI$ 73047,1765.
- They also have an equivalent package for Turbo Pascal, called TPHRT. The
- package is shareware, $20 per copy. This company also sells a fully-functional
- timing toolbox called PCHRT, version 4, which also supports running the timer
- tick interrupt at a user-specified rate, and can be ordered from Ryle Design
- (order form included with MSCHRT V3 and TCHRT V3) for $49.95. This, and
- registered versions of MSCHRT, TCHRT, and presumably TPHRT, include library
- source and support. I have not checked that they are still contactable.
-
- The is clearly a very professionally designed package, which includes thorough
- documentation and has obviously been designed to make the user's task as easy
- and successful as possible. It provides 42 functions including the ability to
- produce formatted reports! It includes a self-calibration function, presumably
- to take into account the different amount of time required to read a timestamp
- on different machines.
-
- The timing functions can operate with interrupts enabled, or disabled (in this
- case, periods longer than 54.925 ms will not be measured correctly). It
- presumably sets CTC channel zero to mode 2, though the manual doesn't describe
- this correctly.
-
- Suggested applications are: timer or profiler to determine code performance,
- benchmarking programs, precise delays for hardware or process control, subject
- testing (e.g. reaction timing, race timing and scoring systems).
-
- The package also supports profiling and reporting on BIOS function interrupts
- (e.g. int 10h video, int 13h disk) - the vector is hooked and logging logic is
- installed, then complete information can be generated for that interrupt.
- Functions specifically to delay a specified length of time are also available.
- The package includes the library file, explanatory material, function reference,
- and five demo programs.
-
- There is no date in the manual, but the newest file in the archive is dated
- 19900723. I did not test this package - it's probably safe to assume that it
- works well.
-
- ## 10.8.3 THE TCTIMER PACKAGE
-
- ftp://oak.oakland.edu/SimTel/msdos/turbo_c/tctimer.zip
- Date: 19891029 Size: 15609
-
- This is a public domain absolute timestamping package for Turbo C. It contains
- functions to enable mode 2 on CTC channel zero, to restore normal operation in
- mode 3 at exit, to read an absolute timestamp, and to calculate elapsed time in
- units of one microsecond using floating point arithmetic. The timestamp value
- is comprised of the count in progress and the bottom 16 bits of the BIOS timer
- tick variable, returned as a long (dword), therefore periods longer than one
- hour cannot be measured (this is mentioned in the documentation file).
- The documentation file says it was "written by Richard S. Sadowsky, 8/10/88,
- Version 1.0, released to the public domain, based on TPTIME.ARC which was
- written by Brian Foley and Kim Kokkonen of TurboPower Software and released
- to the public domain". Source code is included.
-
- This package appears to have the following problems.
- 1. Registers SI and DI are not preserved by the readtimer() function,
- usually causing the calling function to crash if it uses register
- variables.
- 2. The readtimer() function has many unnecessary I/O accesses and is
- fairly slow as a result.
- 3. Timing will be incorrect if the timed period spans a change of day,
- because just before midnight the loword of the BIOS tick count
- counts to 0AF hex then resets. This is not handled by this package.
- I tested the code briefly, after fixing the SI and DI problem, and it appeared
- to work correctly (apart from the midnight problem).
-
- ## 10.8.4 THE MILLISEC PACKAGE
-
- ftp://oak.oakland.edu/SimTel/msdos/c/millisec.zip
- Date: 19911204 Size: 37734
-
- This package was released by Fred C. Smith (uunet!samsung!wizvax!fcshome!fredex)
- and is a modified version of a release by Dean Pentcheff (dean@violet.berkeley
- .edu) which is a modified version of the TCTIMER package (see previous section).
- Source is included.
-
- At some stage in the evolution of this package, the resolution seems to have
- been reduced to one millisecond. Dean Pentcheff's package (the 'missing link'
- :-) apparently returned elapsed time as a floating point number in units of one
- second, with three decimal places. This package returns elapsed time in units
- of one millisecond, to avoid floating point calculations. Also the CTC clock
- has been approximated to 1193000 Hz, resulting in a proportional error of
- 152.254 ppm (0.0152254%; 13.155 seconds per day).
-
- These routines use CTC channel zero in mode 2, as per the TCTIMER package, and
- the timer-reading function is identical to TCTIMER's one. The problems that I
- noted for TCTIMER still apply to this package.
-
- ## 10.8.5 THE MSEC_12 PACKAGE
-
- ftp://oak.oakland.edu/SimTel/msdos/c/msec_12.zip
- Date: 19920319 Size: 8484
-
- This package was released by David Kirschbaum (kirsch@usasoc.soc.mil) and is
- a further modification of the MILLISEC package (see the previous section).
- David has moved the inline assembly stuff into a separate file, and fixed the
- problem with destroying SI and DI, though the rest of the read-timer function
- is the same as that of the TCTIMER package, so the remaining two problems are
- still present.
-
- The package uses one millisecond resolution, and approximates the CTC clock to
- 1193000 Hz, resulting in a proportional error of 152.254 ppm (0.0152254%;
- 13.155 seconds per day).
-
- Source and makefiles for TCC, BCC, and QC are included.
-
- ## 10.8.6 THE ERTIMER PACKAGE
-
- ftp://x2ftp.oulu.fi/pub/msdos/programming/docs/ertimer.zip
- Date: 19950506 Size: 9092
-
- This ZIP file contains a message, a header file, and a C source file for an
- includable timing module that provides a user-selectable number of independent
- timers, each with 0.8381 us resolution, implemented via CTC channel 0 operating
- in mode 2 and using the loword of the BIOS timer tick count variable. Written
- by Ethan Rohrer, comments dated 19941204. Nicely written and fairly well
- commented, but cannot measure times longer than about an hour, and does not
- handle the problem of the CTC count synchronisation with the BIOS tick count,
- nor the midnight wraparound where the loword of the tick count counts to 0AF
- hex then wraps back to zero. Also does not lock out interrupts around hardware
- access sequences. Not reliable.
-
- ## 10.8.7 THE FASTCLOK PACKAGE
-
- ftp://x2ftp.oulu.fi/pub/msdos/programming/docs/fastclok.zip
- Date: 19950506 Size: 2588
-
- This package consists of a C source file and a header file. The package runs
- the timer tick interrupt at 64 times its normal speed, using its own interrupt
- handler which chains to the BIOS handler correctly. Does not lock interrupts
- properly when installing and uninstalling. It installs an atexit() function
- to uninstall the fast timer and restore normal operation. The author does not
- identify him/herself. A comment in the source file says:
- "The gettimeofday() routine acts like the Unix version, with the
- exception that time zone does not matter. The time will be returned
- in timeval structures that match thier Unix counterparts".
- The program doesn't seem to include a gettimeofday() function, though. :-\
-
- ## 10.9 BENCHMARKING CONSIDERATIONS
-
- When using absolute timestamping to benchmark a section of code, remember that
- because interrupts are enabled during execution of the code being timed, they
- will contribute to the time measured.
-
- During otherwise idle time, the timer tick interrupt will be active (every
- 54.9254 ms), the keyboard keystroke interrupt will occur every time a key is
- pressed or released, or repeatedly while the key is held down, and if a mouse
- driver is installed and enabled, the mouse's interrupt will occur several
- times every time the mouse is moved or the buttons change state.
-
- If the code being timed takes a short time, e.g. less than 100 milliseconds,
- the effect of the timer tick interrupt may be detectable. If the period is
- shorter than 54.9 ms, it can be measured with interrupts locked out, because
- interrupts are only required to ensure that the BIOS tick count variable is
- updated correctly on every cycle of CTC channel zero.
-
- The other factors can be avoided by not touching the keyboard or mouse during
- the test.
-
- Other factors have an effect on benchmarks, such as the processor cache state
- and, for file processing programs, the disk cache state. The latter problem
- can be avoided by disabling the disk cache, or ensuring that the input file
- is already in the cache (providing that the cache is big enough to hold it) by
- entering 'copy /b filename nul:' to force the entire file to be read from disk.
-
- Finally, adding the code which reads the timer adds to the execution time.
- For example, if you call a function to read an absolute timestamp twice in
- succession, the times read will differ by the amount of time taken to read the
- timestamp. For example, the assembly language get-timestamp function given
- in the sample program in section »» 9.2 takes between 7 and 9 CTC clocks (about
- 6.5 us) to execute on my 486DX2-66.
-
- I have no experience or information on ways to determine processor clock speed.
- If anyone can help, please let me know. (*)
-
- ## 10.10 GRANULARITY AND UNCERTAINTY
-
- This may seem obvious, but the accuracy of any time measurement is limited by
- the granularity of the timing source, and its uncertainty. Granularity, or
- resolution, refers to the fineness of the unit in which the time or duration
- can be measured. For example, using 54.9254 ms timer ticks to measure the
- time taken by a short section of code is going to be of limited use. On most
- of the test runs, no time will appear to have elapsed, but occasionally, one
- tick, or 54.9254 ms, will appear to have elapsed. The resolution is not high
- enough, and a different approach is required - for example, running the section
- of code repeatedly in a loop, and measuring the total time taken.
-
- If 1000 iterations of the code are timed using the timer tick, by sampling the
- BIOS Tick Count variable, running the code 1000 times, then re-reading the BIOS
- Tick Count and using the difference in tick counts to calculate the amount of
- time elapsed, we might find that five ticks, or about 275 ms, have elapsed, but
- how accurate is this figure?
-
- Code execution ───────────────███████████████████████████████████───────
-
- Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
- 1 2 3 4 5 6 7 8
-
- (You will need a monospaced display to see the above diagram properly).
-
- In the above example, when the code started, the tick count was 2. When it
- finished, the tick count was 7. The execution time was 5 ticks.
-
- Code execution ────────────█████████████████████████████████████████────
-
- Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
- 1 2 3 4 5 6 7 8
-
- Above, when the code started, the tick count had just changed to 2, and when it
- finished, the tick count was 7, just about to change to 8. The measured time
- was 5 ticks, as before, but the actual execution time was nearly 6 ticks.
-
- Code execution ──────────────────█████████████████████████████──────────
-
- Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
- 1 2 3 4 5 6 7 8
-
- Above is the opposite case. The measured time is again from 2 to 7, 5 ticks,
- but the execution time was actually only slightly longer than 4 ticks.
-
- These examples demonstrate uncertainty of up to one tick at both the start and
- the end of the sampling time. The uncertainty at the start of the sample is
- due to the granularity, or resolution, of the timing source, and the fact that
- it is free-running or asynchronous (not synchronised) to the event being timed.
- The uncertainty at the end of the sampling time is the unavoidable effect of
- the resolution of the timing source. The total uncertainty of the sample is
- two ticks.
-
- If we wait for the tick count to change, then start the code, we can eliminate
- (or greatly reduce) the uncertainty at the start of the sampling time.
- The worst cases would then be:
-
- Code execution ────────────███████████████████████████████████──────────
-
- Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
- 1 2 3 4 5 6 7 8
-
- and
-
- Code execution ────────────█████████████████████████████████████████────
-
- Timer ticks ────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───
- 1 2 3 4 5 6 7 8
-
- Sometimes it is possible to synchronise the time reference and the event to be
- timed, either by delaying the start of the event (as in the above example) or
- by starting the time reference from a known part of its cycle when the start of
- the event is detected.
-
- Sometimes it is not possible to do this. For example, the Refresh Detect signal
- described in section »» 7.37 has a period of about 15 us, but cannot safely be
- stopped and restarted at a particular point so that it is synchronised to the
- start of some event. When using such a time base, you must either synchronise
- the event to the time base (as in the third method of reading the joystick
- position in section »» 10.4.4) or live with the fact that there is a 30 us
- uncertainty in any event that is timed using this method.
-
- Also see the sample program section »» 4.7 (timeouts implemented using the
- timer tick) where the uncertainty is actually at the _start_ of the timing
- period, not at the end.
-
- ## 10.11 CONVERTING BETWEEN MICROSECONDS AND CTC CLOCKS
-
- Conversion between microseconds and CTC clocks requires fairly accurate
- arithmetic, namely multiplication by 1.193181666... or 0.838095...
-
- This can be done using floating point, however this is slow on machines without
- a math coprocessor, and is inefficient, and does not necessarily give very good
- accuracy, even if you are not using a Pentium :-) And as floating point is not
- usually required in the remainder of the program, it seems silly to require it
- for this purpose only.
-
- For comparatively painless implementation on all x86 processors under DOS, the
- method described here uses a function that multiplies two long values (32 bits
- each) together, giving a 64-bit result, and returns the top 32 bits of the
- result as a 32-bit long. If bit 31 of the 64-bit result is set, then the
- return value is rounded up.
-
- Using longs to represent microseconds or CTC clocks limits the maximum period
- that can be expressed to about 59 minutes and 59.592 seconds (0xFFFFFFFFL CTC
- clocks), i.e. slightly less than one hour.
-
- The function definition follows. I have used Borland's register pseudovariables
- (_AX and _DX), so this must be changed for other compilers.
-
- -------------------------------- snip snip snip --------------------------------
- unsigned long mul64shift32(unsigned long value, unsigned long mult) {
- asm {
- push si
- mov ax,WORD PTR value
- mul WORD PTR mult
- mov si,dx
- mov ax,WORD PTR value+2
- mul WORD PTR mult+2
- mov bx,ax
- mov cx,dx
- mov ax,WORD PTR value
- mul WORD PTR mult+2
- add si,ax
- adc bx,dx
- adc cx,0
- mov ax,WORD PTR value+2
- mul WORD PTR mult
- add si,ax
- adc bx,dx
- adc cx,0
- shl si,1
- adc bx,0
- adc cx,0
- mov ax,bx
- mov dx,cx
- pop si
- }
- return (_DX << 16) + _AX; /* Should optimise out to nothing */
- }
- -------------------------------- snip snip snip --------------------------------
-
- The arithmetic expression for this function is:
-
- return_value = int ((value * mult / (2^32)) + 0.5)
-
- Note there is no way for overflow to occur in this function, because even with
- value and mult of 0xFFFFFFFFL, the 64-bit result is only 0xFFFFFFFE00000001.
-
- This function can be used in the conversion of microseconds to CTC clocks and
- vice versa, by the appropriate choice of the 'mult' value. The 'mult' value
- is defined as the desired multiplication factor (e.g. 0.838...) multiplied by
- 2^32.
-
- For conversion from CTC clocks to microseconds (multiplication by 0.838095...),
- the 'mult' value is 3599592096L (0xD68D6AA0L). For conversion from microseconds
- to CTC clocks (multiplication by 1.193181666...), 'mult' would be 5124676237,
- which is too large to express as a long (because the factor of 1.193181666...
- is greater than 1), so this conversion is done by multiplying by the fractional
- part of the conversion factor, 0.193181666..., then adding the original value.
- The fractional part of the conversion factor equates to a 'mult' value of
- 829708941L (0x31745A8DL).
-
- Here are the two conversion functions, which use mul64shift32() internally.
-
- -------------------------------- snip snip snip --------------------------------
- unsigned long clocks_to_usec(unsigned long clocks) {
- return mul64shift32(clocks, 3599592096L);
- }
-
- unsigned long usec_to_clocks(unsigned long usecs) {
- if (usecs > 3599592094L)
- return 0xFFFFFFFFL;
- return usecs + mul64shift32(usecs, 829708941L);
- }
- -------------------------------- snip snip snip --------------------------------
-
- Note the check in usec_to_clocks(). The maximum number of microseconds that
- can be represented by a 32-bit number of CTC clocks is 3599592095, which
- equates to 0xFFFFFFFFL CTC clocks. This represents a time of about 59 minutes
- and 59.592 seconds, just under one hour.
-
- Because of the unrelated units of the two quantities, conversion between clocks
- and microseconds using integer values inevitably introduces rounding errors,
- so conversions should not be done cumulatively. For example, if you are
- summing several durations, your measurements should be kept in clocks and
- converted to microseconds after the summation.
-
- Other than integer rounding error, the above functions contribute a proportional
- error of less than 0.00000001% (0.0001 ppm, 9 us per day, about five orders of
- magnitude better than typical crystal accuracy :-).
-
- ## 10.12 MAINTAINING A MILLISECOND OR MICROSECOND COUNT
-
- The sample program in section »» 4.7 uses the BIOS Tick Count variable as a
- time indication. This variable is in units of one tick, i.e. 54.9254 ms.
- There may be cases where you want to maintain a time value which is in units
- of some more sensible value, for example, milliseconds, or maybe microseconds.
-
- Converting between absolute tick count and absolute milliseconds is messy, but
- it is easy to maintain a variable, in units of one millisecond or microsecond,
- which is updated cumulatively using the timer tick. For example, you could
- define a 32-bit variable that will contain the number of milliseconds since
- the program started, and call this the milliseconds variable.
-
- When and where the milliseconds variable is updated depends on your program
- design. The variable needs to be updated every time a timer tick occurs.
- You can achieve this by hooking int 1C hex (see section »» 6.35 and »» 6.36)
- or by hooking int 8 (see section »» 6.33), or if there is a convenient 'idle'
- point in your program where it can read the BIOS tick count variable, the
- update can be done there, by checking whether the tick count has changed from
- the previous tick count, and if so, updating the millisecond variable and
- updating the previous tick count, but with this last method, the logic is a
- bit untidy because the update must behave correctly if more than one tick has
- elapsed since the update routine was last called.
-
- Updating the milliseconds variable involves adding the number of milliseconds
- that have elapsed, into the variable. If CTC channel zero is running with its
- normal divisor of 65536, every timer tick interrupt represents 54.9254 ms of
- elapsed time. But since the milliseconds variable is a 32-bit integer (no
- fractional part), you can't add 54.9254 to it. You have to keep another
- variable that keeps track of the remainder. On most interrupts, you will add
- 55 to the milliseconds variable, but on some interrupts, you will add only 54.
- This can be done using a scheduling variable to control whether the 'add' value
- will be 54, or 55.
-
- On every interrupt, we will add either 54 or 55 to the milliseconds variable.
- But the elapsed time is 54.9254 ms. A remainder variable keeps track of the
- fractional part of the real time, and allows us to decide whether to add 54 or
- to add 55.
-
- The fractional part of the tick period (in milliseconds) is 0.9254, or more
- accurately, 0.9254164984656, which is roughly 12/13. 65536 multiplied by this
- value is about 60648. If we add 60648 to a 16-bit count which represents a
- number of 1/65536ths-of-a-millisecond, every time the addition carries (which
- will be about 12 out of every 13 times), another millisecond has accumulated,
- so we would add 55 to the millisecond variable. If the remainder variable did
- not carry after adding the 60648, we would add 54 to the milliseconds instead.
-
- Over a reasonable period of time, and (most importantly) over a long period of
- time, the milliseconds variable will be accurate. The error contributed by this
- technique (due to approximating 65536 x 0.9254... to 60648) is only 0.02657 ppm,
- or less than one second per year. The error contributed by crystal inaccuracy
- will be about three orders of magnitude higher.
-
- The code to do the update comes out very nicely in assembler:
-
- add Remainder,60648 ; Add 65536 x 0.9254
- adc MillisecL,54 ; Add 54 or 55 to loword
- adc MillisecH,0 ; Carry into hiword
-
- The three variables Remainder, MillisecL, and MillisecH, are all 16-bit.
- MillisecL and MillisecH are loword and hiword of the milliseconds variable.
-
- For a microsecond counter, the same technique applies, but instead of adding
- 54 or 55 on each tick, you are adding 54925, and the remainder is 65536 x
- 0.4164984656, or 27295.
-
- add Remainder,27295 ; Add 65536 x 0.4164984656
- adc MicrosecL,54925 ; Add 54925 or 54926 to loword
- adc MicrosecH,0 ; Carry into hiword
-
- These techniques don't magically give you millisecond or microsecond timing
- resolution from a 54.9254 ms clock tick, of course. The resolution is still
- only 54.9254 ms. But they do provide a way to get a time value with a sensible
- unit.
-
- The same technique can be used when the timer tick is operated at a faster rate
- (see section »» 8 and subsections), though the constants change. For example,
- to get an actual timing resolution of about 500 us, you could use a channel 0
- divisor of 596, giving an interrupt rate of one tick every 499.504825334 us.
- Using a microsecond variable, the update would add 499 plus the carry from
- adding 33084 to the remainder variable, and 499 plus carry to the microseconds
- variable:
-
- add Remainder,33084 ; Add 65536 x 0.504825334
- adc MicrosecL,499 ; Add 499 or 500 to loword
- adc MicrosecH,0 ; Carry into hiword
-
- With these values, cumulative error due to approximation of 65536 x 0.5048...
- to 33084, is 0.00712 ppm.
-
- Choosing to use a millisecond timing variable may make your program easier to
- port to (or from) an environment where the system time is kept in units of one
- millisecond. For example, OS/2's system time is kept in units of 1ms, though
- it does not have a 1ms resolution - is actually only updated every 31.25 ms.
-
- ## 10.12.1 SAMPLE PROGRAM: MILLISECOND COUNT USING INT 1CH
-
- The following program uses int 1Ch with the critical error handling module from
- section »» 5.8, and demonstrates maintaining a milliseconds count. The timing
- resolution of the program is only 54.9254 ms, as it does not modify the timer
- tick rate, but the time is reported in units of one millisecond, rather than
- units of 54.9254 ms.
-
- Int 1Ch should not be used in TSRs - see section »» 6.35 for details.
-
- Every time the user presses a key, the current millisecond count is displayed.
- Pressing the Escape key terminates the program.
-
- -------------------------------- snip snip snip --------------------------------
- /*
- Sample program #20
- Demonstrates a milliseconds count using int 1Ch
- Part of the PC Timing FAQ / Application notes
- By K. Heidenstrom (kheidens@actrix.gen.nz)
-
- Save and assemble the critical error module CRIT_ERR
- Save this sample code to SAMPLE20.C
- Compile this module with:
- bcc -c -I<inc_path> -ms sample20.c
- Link the modules with:
- tlink /c /x <c0_path>\c0s.obj sample20.obj crit_err.obj,
- sample20, nul, <lib_path>\cs
- Where inc_path is the path to your C header files, c0_path is the path to your
- startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB.
- */
-
- #include <dos.h> /* Needed for enable(), disable(), MK_FP() */
- #include <io.h> /* Needed for _open() and _write() */
- #include <stdio.h> /* Needed for printf() */
- #include <stdlib.h> /* Needed for exit() */
-
- #define FALSE 0
- #define TRUE 1
-
- #define STDERR 2 /* DOS handle for standard error */
-
- void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */
- unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */
-
- typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */
-
- intfuncp old_int_1Ch = (intfuncp)0xFFFFFFFFL;
-
- static unsigned int remainder;
- static volatile unsigned long milliseconds;
-
- void abort_cleanup(int dos_is_safe) {
- if (dos_is_safe) {
- if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
- setvect(0x1C, old_int_1Ch);
- old_int_1Ch = (void far *)0xFFFFFFFFL;
- }
- }
- else {
- disable(); /* Probably superfluous */
- if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) {
- *((intfuncp far *)MK_FP(0, 0x1C << 2)) = old_int_1Ch;
- old_int_1Ch = (void far *)0xFFFFFFFFL;
- }
- }
- return;
- }
-
- void interrupt ctrl_c_handler(void) {
- static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n";
- if (is_at_crit_prompt())
- abort_cleanup(FALSE);
- else {
- abort_cleanup(TRUE);
- _write(STDERR, &message, sizeof(message));
- }
- exit(255);
- }
-
- void interrupt new_int_1Ch(void) {
- asm {
- add remainder,60648
- adc WORD PTR milliseconds+0,54
- adc WORD PTR milliseconds+2,0
- }
- return; /* From interrupt */
- }
-
- void intercept_int_1Ch(void) {
- old_int_1Ch = getvect(0x1C);
- setvect(0x1C, new_int_1Ch);
- return;
- }
-
- unsigned long get_milliseconds(void) {
- static unsigned long rv;
- asm pushf;
- asm cli;
- rv = milliseconds;
- asm popf;
- return rv;
- }
-
- void main(void) {
- int n;
-
- milliseconds = 0;
-
- printf("Sample program #20 - Demonstrates millisecond count using int 1Ch\n");
- printf("Part of the PC Timing FAQ / Application notes\n");
- printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n");
-
- crit_err_intercept(); /* Trap critical errors */
- setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */
- intercept_int_1Ch(); /* Intercept int 1Ch */
- printf("Press any key to display current millisecond count\n");
- printf("Press <Esc> to exit\n\n");
- do {
- while (bioskey(1) == 0)
- ;
- n = bioskey(0);
- printf("Millisecond count is: %ld\n", get_milliseconds());
- } while ((n & 0xFF) != 27);
- abort_cleanup(TRUE);
- exit(0);
- }
- -------------------------------- snip snip snip --------------------------------
-
- ## 10.13 NOTES ON MICROSOFT WINDOWS
-
- I have no interest in Windoze, but I have received a few comments regarding
- timing under Windoze which you may find useful.
-
- {TOR} Tor Sjowall (tor@oslonett.no) said (slightly paraphrased):
-
- > A regular 18Hz clock interrupt routine (int 8 or int 1Ch) in a DOS box under
- > Windows works as usual as long as the DOS box has the focus, but when another
- > window has the focus, the DOS box's timer tick interrupt rate slows down to
- > one tick every 800 ms. The tick counter however is incremented as usual.
- > This has driven me crazy...
- >
- > As far as I can gather from the documentation, this is the correct behaviour.
- > The reason being that Windows wants to use as much of the CPU capacity as
- > possible. Also I have not found any 'back door' into the 386 mode timer
- > interrupt routine that will allow my program to catch the ticks.
- >
- > The RTC periodic interrupt sort-of works under Windows. The problem is that
- > each DOS box has its own Virtual Machine, plus one for Windows itself. So
- > all these VMs each get a simulated hardware interrupt from the real 386 mode
- > interrupt handler. This works well enough, but the overhead is large.
- > Actually, Windows has a real ugly wart here: If the hardware interrupt was
- > enabled in the PIC before Windows was started, all the VMs under Windows will
- > get a simulated hardware interrupt with I/O ports trapped, etc etc. If the
- > interrupt was disabled on the PIC, only the VM that enabled the interrupt on
- > the PIC gets the interrupt. There is a text on the Developer CD, 'The Tao of
- > Interupts', that describes this in all its gory detail.
- >
- > The overhead is enormous: the V86 DOS interrupt has only 7% of the throughput
- > of a native DOS interrupt routine. A 90Mhz Pentium is a good idea...
-
- Thanks Tor for that information.
-
- ## 10.14 DOS FILE DATE AND TIME STAMPS
-
- {TOR} also suggested this topic, as it is related to timing.
-
- DOS's FAT (File Allocation Table) file system stores the date and time of last
- modification of every file. Date and time values are each 16 bits (two bytes)
- wide. In the directory structure, the date value is at offset 24 into the
- directory entry, and the time value is at offset 22. In the findfirst/find-
- next structure in the DTA, returned by DOS when findfirst or findnext are
- requested, the date is also at offset 24 and the time at offset 22.
-
- The file date word is constructed as follows.
-
- F E D C B A 9 8 7 6 5 4 3 2 1 0
- * * * * * * * . . . . . . . . . Year minus 1980 (range 0-119)
- . . . . . . . * * * * . . . . . Month (range 1-12)
- . . . . . . . . . . . * * * * * Day of month (range 1-31)
-
- The file time word is constructed as follows.
-
- F E D C B A 9 8 7 6 5 4 3 2 1 0
- * * * * * . . . . . . . . . . . Hours (range 0-23, 24-hour format)
- . . . . . * * * * * * . . . . . Minutes (range 0-59)
- . . . . . . . . . . . * * * * * Seconds / 2 (range 0-29)
-
- Note that the time is only stored with a resolution of two seconds, so the time
- stamp on a file modified at 12:34:56 is the same as the time stamp on a file
- modified at 12:34:57.
-
- The date and time fields can be combined into an unsigned long value (date in
- the hiword, time in the loword) and compared with other date/time fields in the
- same format to see which file is newer or whether the date and time are the
- same.
-
- ## 10.15 DOS AND THE DATE AND TIME
-
- Under DOS, the current date and time is not stored in the DOS kernel, but is
- provided as required, by the CLOCK$ driver. Whenever DOS wants to know the
- date and time, it issues a 6-byte read request to the clock driver, which it
- identifies via the CLOCK bit in the device attribute word. Traditionally the
- driver name is "CLOCK$" but this is not required.
-
- See section »» 3.3 for a replacement CLOCK$ driver that uses the AT's RTC.
-
- The CLOCK$ driver supports reading and writing. Six bytes are always read and
- written. These bytes encode the full date and time. The six bytes of data are
- all in binary form. The structure is:
-
- 0 WORD Number of days since 1st January 1980 (0-up)
- 2 BYTE Minutes (0-59)
- 3 BYTE Hours (0-23)
- 4 BYTE Hundredths of seconds (0-99)
- 5 BYTE Seconds (0-59)
-
- DOS will write to the CLOCK$ driver when int 21h, functions 2Bh or 2Dh (the DOS
- set date and set time functions) are issued. It will read the CLOCK$ driver
- when int 21h, functions 2Ah or 2Ch (DOS get date and get time functions) are
- issued, or when it wants to know the date and time for timestamping a disk file.
-
- The standard CLOCK$ driver supplied with DOS reads the date from the RTC on
- initialisation (i.e. at reboot time), and converts this date to a count of days
- elapsed since 1st January, 1980. It maintains the date internally in this form,
- as this is the form used by DOS in the CLOCK$ read and write function calls.
- The standard CLOCK$ driver uses the BIOS timer tick count variable to keep track
- of the time of day. This variable is set up by the computer's BIOS from the RTC
- time of day, as part of the power-on self-test (POST) procedure, so it is
- correct when DOS boots.
-
- The CLOCK$ driver issues BIOS interrupt 1Ah, function 0, Get Tick Count, every
- time it is asked to supply the current date and time. This function returns a
- flag in AL called the midnight flag. The flag is set by the BIOS int 8 handler
- when the tick count variable wraps around from 1800AFh to 0, and is true the
- _first_ time BIOS int 1Ah function 0 is called following a change of day.
- After the BIOS has reported the flag true, it clears the flag, and it will only
- be true again on the next change of day.
-
- This is also described in section »» 4.2.
-
- When the date and time are requested from the CLOCK$ driver, it calls the BIOS
- function, checks the midnight flag and if set, increments its count of days
- since 1st January 1980. It then converts the tick count into hours, minutes,
- seconds, and hundredths of seconds. Of course the tick count has a resolution
- of only 54.9254 ms, much more coarse than the 10ms resolution provided by the
- hundredths of seconds value. I do not know what algorithm the CLOCK$ driver
- uses to calculate the hours, minutes, seconds, and hundredths from the tick
- count.
-
- When the date and time are set, the CLOCK$ driver's days since 1980 count is
- set, and the CLOCK$ driver presumably calculates an appropriate tick count value
- and uses int 1Ah function 1 to set the tick count. I believe that the CLOCK$
- driver also updates the RTC date and time, presumably through int 1Ah functions
- 3 and 5.
-
- The DOS kernel contains the code to convert between days, months, and years,
- and number of days since 1st January 1980. It can convert both ways, as the
- date is both requested (int 21h function 2Ah) and set (int 21h function 2Bh)
- in days, months, and years format.
-
- So to summarise, at reboot, the BIOS sets up the tick count using the RTC time,
- DOS boots and the CLOCK$ initialisation code calculates the number of days since
- 1 January 1980 from the RTC date and stores this internally. When the date and
- time is requested from the CLOCK$ driver, it calls int 1Ah function 0, checks
- the returned midnight flag and increments its day count if set, calculates the
- hours, minutes, seconds, and hundredths values from the tick count, and returns
- these values. When the date and time is set by writing to the CLOCK$ driver,
- the driver updates its day count to the specified value, and presumably
- calculates an appropriate tick count, sets it via int 1Ah function 1, sets the
- RTC time via int 1Ah function 3, and calculates days, months, and years from
- the day count and sets the RTC date via int 1Ah function 5.
-
- If anyone has disassembled any DOS CLOCK$ drivers, please let me know what you
- found out. (*) I will eventually do this anyway.
-
- ## 10.15.1 DOS DATE ROLLOVER BUGS
-
- There are two problems related to the change of day under DOS's CLOCK$ driver.
-
- The first is that int 1Ah, function 0, only returns the midnight flag set the
- first time it is called following a change of day. If an application program
- or TSR calls this function, and happens to call it after a change of day but
- before the CLOCK$ driver calls it, the application or TSR will see the change
- of day but the CLOCK$ driver will miss it. Therefore, no program should use
- int 1Ah function 0. Also see section »» 4.2.
-
- The second problem is that there is no way to tell how many midnights have
- passed, since the midnight flag is just a flag, not a count. This problem
- usually affects computers that run constantly but are unattended, where the
- date and time may not be requested for a long time - more than 24 hours. Two
- or more midnights may pass, but when the date and time is requested, the date
- in the CLOCK$ driver is only incremented by one. I have heard that some BIOSes
- actually implement the midnight flag as a count, and the CLOCK$ driver may
- possibly respond to values other than 0 or 1 and update the date correctly,
- but I don't know for sure. (*)
-
- ## 10.16 SIMULATING A VERTICAL RETRACE INTERRUPT
-
- The aim of this technique is to provide an interrupt which is synchronised to
- the screen refresh (i.e. vertical scan) so that certain functions that must be
- performed during the vertical retrace period can be done via this interrupt,
- in the background. For more details on this, see section »» 7.33.
-
- Some video cards (notably EGA cards) are able to generate a vertical retrace
- interrupt themselves, usually on IRQ2/9, but this facility is not standard on
- VGA cards. The vertical retrace interrupt can be simulated using CTC channel 0.
-
- Vertical retrace emulation is sometimes a hot topic on comp.os.msdos.programmer
- and comp.lang.asm.x86, with many people interested in how it can be done, but
- I (and my correspondent Anders Roar Nielsen, aroni@night.ping.dk) don't believe
- that it is necessary in most applications. Retrace can be detected by polling,
- the field time can be measured, and CTC channel 0 can be used to estimate where
- the video circuitry is 'up to' (at the default divisor, there are at least three
- field scans per CTC channel 0 wraparound). These techniques of maintaining code
- synchronisation to the screen refresh, using the CTC, will generally have much
- lower overhead and less impact on other aspects of the machine's performance.
-
- The triple buffering technique, described in section »» 10.16.3, does rely on
- vertical retrace interrupt simulation, however.
-
- ## 10.16.1 VERTICAL RETRACE INTERRUPT SIMULATION DESCRIPTION
-
- The technique described in this section is based on an apparently well-known
- algorithm. I saw it suggested by Tommy Marshall (tommym@oneworld.owt.com), in
- his message <3vv8n1$j8g@paperboy.owt.com> of Sat 05 Aug 1995. I have enhanced
- the algorithm to improve its performance under adverse conditions, and added
- thorough documentation. In his posting, Tommy mentions that some demo source
- code is available on his web site: http://www.owt.com/users/tommym/index.html.
- Thanks to Anders Roar Nielsen (aroni@night.ping.dk) for his help with this
- subject.
-
- The following diagram will only make sense if viewed on a monospaced screen.
-
- ├───── 17088 ─────┼───── 17088 ─────┼───── 17088 ─────┼───── 17088 ─────┤
- ├──── 16968 ────┤ ├──── 16968 ────┤ ├──── 16968 ────┤ ├──── 16968 ────┤ ├──
- ┌┐ . ┌┐ . ┌┐ . ┌┐ . ┌┐
- ││ . ││ . ││ . ││ . ││
- ││ . ││ . ││ . ││ . ││
- ───┘└────────────────┘└────────────────┘└────────────────┘└────────────────┘└─
- │ │ │ │ │ │ │ │ │ │ │ │ │ │
- a b * c d e f g h i j k l m
-
- The above diagram shows the retrace signal graphed against time (time is on the
- horizontal axis). The numbers on the diagram are in units of one CTC clock
- (0.8381 us). The values are as measured on my Tseng ET4000 W32i card operating
- in standard 25-line, 80-column colour VGA text mode (720x400 pixels). The
- pulses are the retrace indication from the video card, readable on the video
- status input port. At point A, when the retrace indication becomes active,
- the entire screen has been scanned and the electron beam is beginning its
- vertical retrace - retracing its steps back to the top of the screen.
- The retrace pulses are fairly short - in this case, only 76 CTC clocks, or
- about 64 us long. The actual vertical retrace time (the time taken for the
- electron beam to return to the top of the screen and start scanning the
- displayable part of the picture) is much longer than the pulse indicates;
- Klaus Hartnegg (klaus@mailserv.brain.uni-freiburg.de) reports that a typical
- VGA vertical blanking period is about 2 ms. The visible part of the next
- vertical scan starts a little later - say at point B. At point D, the scan
- ends and the next retrace begins, ready for the next scan which starts at E.
-
- During the retrace period, i.e. between point A and point B, it is safe to
- modify certain parameters in the video subsystem, for example to perform page
- flipping or some types of screen updates, without causing flicker or other
- visible interference.
-
- ## 10.16.1.1 MEASURING THE FIELD TIME
-
- The field time, i.e. the time span between the same edge of two adjacent retrace
- pulses, can be measured by initialising CTC channel 0 in mode 2, waiting for a
- rising (or falling) edge on the retrace indication, reading the count in CTC
- channel 0, waiting for the next edge of the same type, and re-reading the count
- in CTC channel 0, and calculating the number of CTC clocks elapsed between the
- two samples. This is done with interrupts disabled. In this case, a time of
- 17088 CTC clocks was measured, with a fluctuation of +/- 1 CTC clock period.
- The field period is this number multiplied by 0.8381 us (the CTC clock period).
- In this case the field period is 14.321 ms. The field rate (number of fields
- per second) is the reciprocal of the field time - in this case, about 69.8
- fields per second, i.e. a vertical scan rate of about 69.8 Hz.
-
- ## 10.16.1.2 CONTROLLING THE CTC INTERRUPT
-
- Having determined the field period, 17088 CTC clock periods, we can program CTC
- channel 0 to give an interrupt a short time before the rising edge of the next
- retrace pulse will be due. We wait for a start of retrace, i.e. point A, and
- immediately reset and program CTC channel 0 for mode 2 with a count of slightly
- less than 17088, so that it will generate an interrupt shortly before the start
- of the next retrace, say at point C.
-
- During normal operation, CTC channel 0 will issue an interrupt at point C. The
- int 8 handler will loop, waiting for the start of the retrace (point D). When
- this occurs, the interrupt handler resets and reprograms CTC channel 0, so that
- it will interrupt again at point F. The important screen updates that must be
- done during retrace, can now be performed. The interrupt handler then exits,
- and the mainline gets execution until point F, at which point the CTC triggers
- another interrupt and the cycle repeats as if from point C.
-
- This technique assumes that the video mode does not change during execution and
- is not reset. A video mode reset may cause the scanning to restart out of sync.
- The interrupt will resynchronise in the latter case, but may lock interrupts for
- an unusually long amount of time when doing so, as it may potentially remain in
- the retrace wait loop for a long time.
-
- ## 10.16.1.3 SIGNIFICANCE OF THE SAFEMARGIN VALUE
-
- The number of CTC clocks which are subtracted from the field period to give the
- CTC count value is important. I will call it the SafeMargin value. The sample
- program uses a default SafeMargin of 120 CTC clocks, or about 100 us.
- The significance of the SafeMargin value is that it determines the maximum
- interrupt latency that can be tolerated. This latency is made up of interrupt
- acceptance delay (due to interrupts being locked out) and interrupt overhead
- (e.g. overhead caused by EMM386).
-
- If, for example, interrupts were locked out at point '*', by a CLI instruction
- issued by the mainline or some code that was called by the mainline, then the
- interrupt would be signalled (by the CTC) at point C, but would not be accepted
- immediately. If the acceptance was delayed past point D, the start of the
- retrace period, then the int 8 handler is going to see that the retrace has
- already started. If this occurs, the int 8 handler cannot guarantee that there
- is enough time for the screen manipulation, before the visible part of the scan
- begins and the manipulation will cause visible interference.
-
- Therefore, if interrupts are being locked out periodically by the mainline or
- code called by the mainline, the SafeMargin value must be long enough to cover
- the longest period for which interrupts will be locked out, plus any delays in
- interrupt acceptance (EMM386 overhead), so that in the worst case, if interrupts
- were locked out just before the CTC channel 0 interrupt was signalled, they will
- be enabled in time for the interrupt to be accepted and the int 8 handler to be
- entered and to check the retrace flag _before_ the retrace actually starts, so
- that there is almost the entire retrace period available for the screen update.
-
- The sample program allows the SafeMargin value to be set from the command line
- via a decimal number which represents the number of CTC clocks (units of 0.8381
- us) for the SafeMargin value. The default SafeMargin value is 120, giving a
- safety margin of about 100 us including interrupt overhead.
-
- If you use a short SafeMargin value, it is essential that no foreground code
- locks out interrupts for any reasonable length of time. On the other hand, a
- large SafeMargin value reduces the amount of time available for other processes
- (i.e. the mainline), as a larger amount of time is spent in the loop in the int
- 8 handler, waiting for the start of retrace, between points C and D.
-
- If SafeMargin is increased to more than about half of the vertical scan time,
- the system falls apart, giving widely varying loop counts and a jumpy display.
- I haven't bothered to try to figure out why this occurs, because half a field
- period is normally at least 6 ms, so SafeMargin should never be anywhere near
- this long, but I noticed that it can be fixed by moving the instructions that
- prepare the CTC to accept its new count, back to just after the CTC count in
- progress is read, so that the CTC is frozen during the wait-for-retrace loop.
-
- ## 10.16.1.4 OVERHEAD DUE TO LARGE SAFEMARGIN AND SCREEN UPDATE
-
- Depending on the SafeMargin value you choose, you may also need to take into
- account the time spent between points C and D, as it will take a chunk of
- processing time, and operates with interrupts locked out. Remember that the
- operations performed by the retrace function (screen updates, etc) are also
- performed with interrupts locked out, so if they are extensive, this may have
- a significant impact on latency for other interrupts.
-
- For example, don't try to use this technique in a game that communicates via a
- serial link or a modem (multi-player multi-computer games) unless you're using
- a very low data rate, or carefully controlling the outgoing flow control lines
- to prevent loss of incoming characters!
-
- ## 10.16.1.5 ENHANCED HANDLING OF MISSED RETRACE START
-
- The above algorithm can be improved in cases where the start of retrace is
- missed due to interrupt latency. First, if we keep track of the last reload
- value that was programmed into the CTC, we can read the count in progress in
- the CTC and subtract it from that value, to determine the number of CTC clocks
- by which the interrupt was delayed. By subtracting our SafeMargin value, we
- can determine how many CTC clocks into the retrace period we are. We may be
- able to make use of a retrace interrupt even if it was delayed past the start
- of retrace, if we know that there is still enough time to execute screen update
- code before the start of the next visible scan. Also, we can correct for the
- error by reprogramming CTC channel 0 even if we cannot get a timing reference
- from the video subsystem. I will now explain these enhancements in detail,
- using an example.
-
- ├──── 16968 ────┤ │
- ┌──┐ . ┌──┐ . ┌──┐
- │ │ . │ .│ . │ │
- │ │ . │ .│ . │ │
- ───┘ └──────────────┘ .└──────────────┘ └──
- │ │││ │ │ │
- a bcd e f g
-
- Using the above diagram as an example, assume that the field period is 17088
- clocks, SafeMargin is 120, and the visible scan starts at point E. The CTC
- was programmed with a count of 16968. An interrupt is signalled at point A
- but interrupts are locked out by the mainline or some code called by the
- mainline. At point B, retrace has already started, but our interrupt routine
- is still prevented from executing. Then interrupts are enabled at point C.
- The interrupt handler starts immediately, but discovers that retrace has already
- started. It reads the CTC count in progress at point D, and gets a value of,
- say, 16728. At point A, the CTC reloaded with a count of 16968, so by
- subtracting the count in progress from the last CTC count, i.e. 16968-16708,
- which is 260 CTC clocks, we can calculate the number of CTC clocks between
- point A and point D (point D being _now_). This value is the amount of time
- by which the interrupt was delayed, and includes delay caused by interrupts
- being locked out, and delay in the actual interrupt acceptance process, e.g.
- delay caused by EMM386. I will call this value the Latency value.
-
- We can then subtract SafeMargin (which is a fairly accurate estimate of the
- time between points A and B) from the Latency value, to calculate the time
- between point B (start of retrace) and point D (now). This gives a result of
- 140 clock cycles between B and D. If the start of visible scan at point E is
- known to be, say, 1300 CTC clocks after point B (the start of retrace), we can
- calculate how much time remains before the visible scan begins, by subtracting
- the number we just calculated (140, the time between B and D) from the 1300
- (the time between B and E), to get 1160 CTC clocks, the amount of time left
- before the visible scan starts. If the screen update code is known to take
- comfortably less than this amount of time, then it can still be executed.
-
- If that was tricky, it gets trickier! If interrupt acceptance was delayed
- so long that the interrupt routine executed after the _end_ of the retrace
- pulse, it would not know that it had missed the pulse altogether, and would
- sit in its wait loop, waiting for the start of retrace, for the entire
- displayed field, until the _next_ retrace started! During this time, the
- mainline could not execute, and interrupts would be locked out! We can detect
- this, again by reading the CTC count in progress on entry to the int 8 handler
- and determining how long the interrupt acceptance was delayed (i.e. the Latency
- value). If the Latency is significantly longer than SafeMargin, we can assume
- that we have at least missed the _start_ of the retrace, and possibly the end
- as well.
-
- When the interrupt is accepted within the SafeMargin period, we can wait for
- the start of retrace, then resynchronise the CTC by resetting it and setting
- the count to the field period minus SafeMargin (16968) again. But when we miss
- the start of retrace, because interrupt acceptance was delayed for longer than
- SafeMargin, we no longer have a video timing reference from which we can
- resynchronise CTC channel 0. But since we know how long interrupt acceptance
- was delayed (the measured Latency value), we can estimate a new count to
- program into CTC channel 0, that will cause an interrupt at roughly the correct
- point in the next cycle.
-
- For example, in the above example, at point D we know that 260 clocks have
- elapsed since point A when the interrupt was signalled by the CTC. We want
- the next interrupt to be signalled at point F, which is SafeMargin clocks
- before the start of the next retrace, at point G. The count to be programmed
- into CTC channel 0 is therefore the field time minus the measured Latency.
- CTC channel 0 is reset and programmed with a count of 17088 - 240, or 16848.
- 16848 CTC clocks later, it will generate the interrupt at point F.
-
- This method is not 100% accurate, as there will be some delay in reading and
- setting the CTC count, as well as some delay between the two. This would
- result in the interrupts getting progressively later, if retraces were missed
- repeatedly. As soon as an interrupt is accepted within SafeMargin, the CTC
- is resynchronised from the retrace signal. I thought of a better method that
- would have kept better synchronisation in these circumstances, and it should
- have worked, but it didn't. Oh well.
-
- ## 10.16.1.6 OTHER NOTES
-
- There may be special considerations for interlaced video modes. If you are
- using these modes, you will probably already know enough to figure out whether
- there will be any problems :-)
-
- Also, because this technique intercepts int 8, the standard precautions as
- described in section »» 5 should be taken, to ensure that the program is not
- terminated without being able to clean up and restore the original int 8
- handler and standard divisor on CTC channel 0.
-
- I found it very interesting to run various programs from the DOS shell and see
- the effect they have on interrupt latency. For example, on my 486DX2-66, the
- background interrupt latency is typically 15 to 20 CTC clock cycles, and no
- retraces are missed at SafeMargin = 120. My COMSPEC points to a file on a RAM
- drive - if my command processor was on the hard disk, and was uncached, the
- interrupt latency due to the DOS EXEC call that invokes the command processor
- would probably make it impossible to determine the background latency.
- The DOS EXEC call could be replaced with a delay loop, to determine the
- background latency in this case (if you wanted to).
-
- Listing a directory increases the longest int 8 latency slightly, but few if
- any retraces are missed. But, running CHKDSK gives a longest latency of about
- 7000 to 8000 CTC clocks, and many missed retraces. With the SMARTDRV.SYS disk
- cache installed, after CHKDSK has run once, it does not need to physically
- access the hard drive again, and the maximum interrupt latency drops to about
- 80 CTC clocks, with no missed retraces (SafeMargin = 120).
-
- ## 10.16.2 SAMPLE PROGRAM: SIMULATING A VERTICAL RETRACE INTERRUPT
-
- -------------------------------- snip snip snip --------------------------------
- NAME SAMPLE21
-
- ; Sample program #21
- ; Demonstrates a simulated vertical retrace interrupt
- ; Part of the PC Timing FAQ / Application notes
- ; By K. Heidenstrom (kheidens@actrix.gen.nz)
- ;
- ; This program assembles into SAMPLE21.COM, a program which implements an
- ; simulated vertical retrace interrupt using CTC channel 0. It installs its
- ; interrupt handler, and shells to DOS, allowing other programs to be run
- ; while its interrupt handler is installed. The interrupt handler causes the
- ; screen text to move up and down, in a seasickness-inducing fashion.
- ;
- ; This program requires a VGA card able to operate in video mode 3 (80x25,
- ; colour mode) but does not check that this is present.
- ;
- ; Save this file to SAMPLE21.ASM and assemble with:
- ; masm SAMPLE21;
- ; link SAMPLE21;
- ; exe2bin SAMPLE21.exe SAMPLE21.com
- ; or
- ; tasm SAMPLE21;
- ; tlink /t SAMPLE21;
- ;
- ; The techniques used in this program cannot safely be used in a TSR or a
- ; program that shells to DOS in a general way to run any DOS program. This
- ; technique is intended to be used as part of an application, where the
- ; behaviour of the 'foreground' code is known and controlled as much as
- ; possible. Though this program does shell to DOS, it is not intended to be
- ; used to run all types of programs. The shell to DOS feature is just to
- ; demonstrate that the screen updates are in fact being done under interrupt.
- ;
- ; This program can be assembled with or without the performance monitoring and
- ; reporting capability. Set the REPORT conditional to 0 for no performance
- ; monitoring and reporting, or to 1 for performance monitoring and reporting.
- ; Additional code in the int 8 handler is enabled if REPORT is enabled.
- ; The performance and behaviour monitoring functions will often be useless in
- ; production code.
-
- REPORT = 1 ; Enable for report stuff
-
- Code SEGMENT
- ASSUME cs:Code,ds:Code
-
- ORG 100h
- Main: jmp Main2
-
- SignOnMsg DB 13,10,"SAMPLE21 -- Demonstrates simulated vertical retrace interrupt",13,10
- DB "Part of the PC Timing FAQ / Application notes",13,10
- DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10
- DB "Usage: SAMPLE21 [Safety-margin]",13,10,13,10
- DB "This program assumes, but does not check for, a VGA card in 80x25 mode",13,10,13,10
- DB "Type EXIT at the DOS prompt to quit this program",13,10,13,10,"$"
- ComspecMsg DB "SAMPLE21: Can't locate COMSPEC in environment",13,10,"$"
- IF REPORT
- Msg0 DB 13,10," Chosen safety margin: $"
- Msg1 DB " CTC clocks",13,10," Measured field time: $"
- Msg2 DB " CTC clocks",13,10," Total retraces: $"
- Msg3 DB 13,10,"Missed retrace starts: $"
- Msg4 DB 13,10,"Longest int 8 latency: $"
- Msg5 DB " CTC clocks",13,10," Longest retrace wait: $"
- Msg6 DB " loops",13,10,"Shortest retrace wait: $"
- Msg7 DB " loops",13,10,"$"
- ENDIF
-
- ComSpecTxt0 DB "COMSPEC=" ; Text to find COMSPEC in environment
- ComSpecTxtL = $ - ComSpecTxt0 ; Length of same
- ComspecPtr DW 0 ; Pointer to COMSPEC in environment
-
- ALIGN 2
-
- ExecParmBlock: ; EXEC parameter block
- EnvirSeg DW 0 ; Segment-paragraph of environment
- DW ShellCommand ; Pointer to command line
- SetToCS1 DW 0 ; Segment part for above
- DW 5Ch ; Let it use our FCBs
- SetToCS2 DW 0 ; Segment part again
- DW 6Ch ; Ditto
- SetToCS3 DW 0 ; Ditto
-
- ShellCommand DB 0,13 ; Command tail length and contents
-
- IF REPORT
- ReportTbl DW MsgPrint,Msg0
- DW PrintDec,SafeMargin
- DW MsgPrint,Msg1
- DW PrintDec,FieldPeriod
- DW MsgPrint,Msg2
- DW PrintDec,Retraces
- DW MsgPrint,Msg3
- DW PrintDec,MissedStarts
- DW MsgPrint,Msg4
- DW PrintDec,MaxLatency
- DW MsgPrint,Msg5
- DW PrintDec,Longest
- DW MsgPrint,Msg6
- DW PrintDec,Shortest
- DW MsgPrint,Msg7
- DW Continue,0
- ENDIF
-
- SafeMargin DW 120 ; Interrupt 100us early (default)
- FieldPeriod DW 0 ; Number of CTC clocks in each field
- ; (frames per second = 1,193,181.66666... / FieldPeriod)
- LastCTC DW 0 ; Last CTC count programmed
- Latency DW 0 ; Actual latency for this interrupt
- Int8Sched DW 0 ; Scheduler for calling BIOS int 8
- IF REPORT
- First DW 1 ; Flag whether first retrace
- Retraces DW 0 ; Count of retraces
- MissedStarts DW 0 ; Count of missed retrace starts
- MaxLatency DW 0 ; Worst int 8 delay (CTC clocks)
- Longest DW 0 ; Longest retrace wait (loops)
- Shortest DW 0FFFFh ; Shortest retrace wait (loops)
- ENDIF
- Cycler DW 0 ; Cycle control variable
-
- ; The sinewave table was created using the following GW-BASIC program using a
- ; number of entries of 64, range of values of 16, and centre offset of 7.5.
- ; The program generated one '16' in the middle of the '15' values, but I just
- ; manually fixed this to 15. A value of 16 causes the screen to jump.
- ;
- ;10 PRINT"This program will generate a sinewave table. The table is written to"
- ;20 PRINT"a disk file called SINE.DMP. The file is in text form, and contains"
- ;30 PRINT"one line per entry, in ASCII decimal representation. All entries are"
- ;40 PRINT"integers. Parameters required are: number of entries in table, range"
- ;50 PRINT"of values (peak to peak), and centre offset. One cycle of sine wave"
- ;60 PRINT"is written.":PRINT:INPUT"Number of entries in table :",NE#
- ;70 INPUT"Peak to peak value range :",PP#:INPUT"Zero offset :",ZO#
- ;80 OPEN "SINE.DMP" FOR OUTPUT AS#1 : A# = 0 : I# = 6.283185307179586#/NE#
- ;90 FOR P = 1 TO NE# : S# = SIN(A#) : V = INT((S# * PP# / 2) + ZO# + .5#)
- ;100 PRINT #1,V : A# = A# + I# : NEXT : CLOSE #1 : SYSTEM
-
- CycleTbl DB 8,8,9,10,11,11,12,13,13,14,14,15,15,15,15,15
- DB 15,15,15,15,15,15,14,14,13,13,12,11,11,10,9,8
- DB 7,7,6,5,4,4,3,2,2,1,1,0,0,0,0,0
- DB 0,0,0,0,0,0,1,1,2,2,3,4,4,5,6,7
-
- Main2 PROC near
- cld ; Upwards string direction
- mov si,81h ; Command tail
- Loop1: lodsb ; Get character
- cmp al,13 ; C/R yet?
- je NoParam ; If so
- cmp al," " ; Whitespace?
- jbe Loop1 ; Loop if so
-
- ; Parse decimal number parameter to replace default SafeMargin value
-
- xor bx,bx ; Clear calculated value
- ReadNumLp: sub al,"0" ; Convert "0"-"9" to 0-9
- cmp al,9 ; Check for valid char
- ja ReadNumFin ; If not, terminator
- cbw ; Zero AH
- xchg ax,bx ; New digit to BL, old total to AX
- mov dx,10 ; Ten to unused register
- mul dx ; Multiply old value by ten
- add bx,ax ; Add to new digit
- lodsb ; Read char from command tail
- jmp SHORT ReadNumLp ; Loop for more
- ReadNumFin: mov [SafeMargin],bx ; Store adjustment value
-
- NoParam: mov es,[ds:2Ch] ; Get segment of environment
- mov [EnvirSeg],es ; Set up for command processor
- xor di,di ; Start at start of environment
- ScanEnvLoop: mov si,OFFSET ComSpecTxt0 ; Point at 'COMSPEC='
- mov cx,ComSpecTxtL ; Get length to compare
- push di ; Keep pointer to start
- repe cmpsb ; Compare to 'COMSPEC='
- pop cx ; Restore pointer to start
- je GotComspec ; If found it
- mov di,cx ; Go to start of entry again
- mov cx,8000h ; Maximum length to scan
- xor al,al ; Null terminator to scan for
- repne scasb ; Scan for null terminator
- jne EnvirError ; If error in environment
- cmp BYTE PTR es:[di],0 ; Final entry in environment?
- jne ScanEnvLoop ; If not, keep looking
- EnvirError: mov dx,OFFSET ComspecMsg ; Point to message
- mov ah,9
- int 21h ; Display it
- mov ax,4C01h ; Errorlevel 1
- int 21h
- int 20h ; In case DOS-1 (!)
-
- GotComspec: mov [ComspecPtr],di ; Store offset into environment
- mov [SetToCS1],cs ; Set up segment-paragraphs in EXEC
- mov [SetToCS2],cs ; parameter block for command
- mov [SetToCS3],cs ; interpreter
-
- ; Relocate stack and shrink memory allocation
-
- push cs
- pop es ; ES to Code
- mov sp,OFFSET StackTop ; Relocate stack
- mov bx,OFFSET FreeSpace+15 ; Account for partial paragraph
- mov cl,4 ; Shift count
- shr bx,cl ; Shift to paragraph count
- mov ah,4Ah
- int 21h ; Shrink memory to minimum necessary
-
- ; First, set the VGA card to the required mode. It must be a colour mode,
- ; otherwise the retrace flag appears in a different I/O port and the code
- ; will fail. Any required mode tweaking would be done here, too.
-
- mov ax,3 ; Screen mode
- int 10h ; Set screen mode
- mov dx,OFFSET SignOnMsg ; Point to sign-on message
- mov ah,9
- int 21h ; Display it
-
- ; Set CTC channel 0 for a known mode and reload value - mode 2, 65536.
-
- cli ; No interrupts here please
- mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin
- out 43h,al ; Prepare channel 0 for new divisor
- jmp SHORT $+2 ; Short delay
- xor al,al ; Divisor is 0 (65536)
- out 40h,al ; Write lobyte of divisor
- jmp SHORT $+2 ; Short delay
- out 40h,al ; Write hibyte of divisor
-
- ; Time the number of CTC clocks between two retraces
-
- call StampRetrace ; Load the processor cache
- call StampRetrace ; Wait for start of retrace, read CTC
- mov bx,ax ; Keep it
- call StampRetrace ; Do the same again
- sub ax,bx ; Calculate difference
- mov [FieldPeriod],ax ; Store retrace period (in CTC clocks)
-
- ; Calculate the value to be programmed into CTC channel 0 from now on
-
- sub ax,[SafeMargin] ; Subtract the desired safety margin
- mov [LastCTC],ax ; Store as last programmed value
-
- ; Program the timer to interrupt just before the next retrace starts
-
- xchg ax,dx ; To DX
- mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin
- out 43h,al ; Prepare channel 0 for new divisor
- jmp SHORT $+2 ; Short delay
- mov al,dl ; Lobyte of divisor
- out 40h,al ; Write lobyte of divisor
- jmp SHORT $+2 ; Short delay
- mov al,dh ; Hibyte of divisor
- out 40h,al ; Write hibyte of divisor
-
- sti
-
- mov ax,3508h
- int 21h ; Get int 8 vector
- mov [Old8Ofs],bx ; Store offset
- mov [Old8Seg],es ; Store segment
- mov dx,OFFSET New8 ; Point to new handler
- mov ax,2508h
- int 21h ; Set vector
- push cs
- pop es ; ES back to Code
-
- ; Now execute the command processor
-
- mov bx,OFFSET ExecParmBlock ; Point to EXEC parameter block
- mov dx,[ComspecPtr] ; Get offset to command specification
- mov ds,[EnvirSeg] ; Get segment of environment
- ASSUME ds:nothing
- mov ax,4B00h
- int 21h ; Execute command interpreter
- push cs
- pop ss ; Restore SS
- mov sp,OFFSET StackTop ; Reset stack
- push cs
- pop ds ; Restore DS
- ASSUME ds:Code
-
- ; Restore VGA CRTC register 8 to its default value
-
- mov dx,3D4h ; Address VGA CRTC
- mov ax,8 ; Register number and value (0)
- out dx,ax ; Restore it
-
- ; Restore normal mode and divisor in CTC channel 0
-
- cli ; No interrupts around this bit
- mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3
- out 43h,al ; Prepare channel 0 for new divisor
- jmp SHORT $+2 ; Short delay
- xor al,al ; Divisor is 0 (65536)
- out 40h,al ; Write lobyte of divisor
- jmp SHORT $+2 ; Short delay
- out 40h,al ; Write hibyte of divisor
- sti ; Interrupts are OK now
-
- lds dx, [DWORD PTR Old8Ofs] ; Get old int 8 handler
- ASSUME ds:nothing
- mov ax,2508h
- int 21h ; Restore int 8 vector
- push cs
- pop ds ; DS back to Code
- ASSUME ds:Code
-
- ; Generate report if REPORT conditional enabled
-
- IF REPORT
- cld ; Just make sure
- mov si,OFFSET ReportTbl
- ReportLp: lodsw ; Handler address
- xchg ax,cx ; To CX
- lodsw ; Parameter
- xchg ax,bx ; to BX
- mov ax,[bx] ; Get value (if applicable)
- mov dx,bx ; Pointer to DX
- call cx ; Call handler
- jmp SHORT ReportLp ; Loop
- Continue: pop ax ; Fix up stack
- ENDIF
-
- mov ax,4C00h
- int 21h
- Main2 ENDP
-
- ; This function waits for the start of a vertical retrace then reads the count
- ; in progress in CTC channel 0. It assumes a VGA card, running in a colour
- ; mode. It also assumes CTC channel 0 is operating in lobyte-hibyte access
- ; mode and operating mode 2, and returns the count in AX, converted to an
- ; up-count. It first waits for any retrace currently in progress to end, then
- ; waits for the next retrace to start and immediately reads the CTC count.
- ; This function must be called with interrupts disabled. Destroys AX and DX.
-
- StampRetrace PROC near
- mov dx,3DAh ; VGA status port in colour modes
- WaitRetr1: in al,dx ; Read status
- test al,00001000b ; Check retrace flag
- jnz WaitRetr1 ; If set, we are already in a retrace
- WaitRetr2: in al,dx ; Read status
- test al,00001000b ; Check retrace flag
- jz WaitRetr2 ; If clear, keep waiting for retrace
- xor al,al ; Command to latch channel 0
- out 43h,al
- jmp SHORT $+2 ; Short delay
- in al,40h ; Read lobyte of count in progress
- jmp SHORT $+2 ; Short delay
- mov ah,al ; Keep it in AH
- in al,40h ; Read hibyte of count in progress
- xchg al,ah ; To correct registers
- neg ax ; Convert to up-count
- ret ; Return in AX
- StampRetrace ENDP
-
- ; This function prints AX in ASCII decimal representation. Output is via DOS
- ; function 2. AX, BX, CX, and DX are all destroyed.
-
- PrintDec PROC near
- xor cx,cx ; Zero digit counter
- PrintDec1: xor dx,dx ; Clear high word of value in DX|AX
- mov bx,10 ; Base
- div bx ; Divide by 10
- add dl,"0" ; DL is remainder, convert to ASCII
- push dx ; Store on stack
- inc cx ; Increment char counter
- test ax,ax ; Any more digits left?
- jnz PrintDec1 ; If so, loop
- PrintDec2: pop dx ; Get char back
- mov ah,2 ; Print char
- int 21h ; Call DOS
- loop PrintDec2 ; Loop for all chars
- ret ; Done
- PrintDec ENDP
-
- MsgPrint PROC near ; Print message pointed to by DX
- mov ah,9
- int 21h ; Print message ('$' terminated)
- ret
- MsgPrint ENDP
-
- ; The following function is the replacement int 8 handler. There is a lot of
- ; conditional code that is enabled by the REPORT conditional. The version
- ; with reporting is very instructive, and useful during development, but you
- ; may prefer to base production code on the version without the performance
- ; monitoring code.
-
- ASSUME ds:nothing
-
- New8 PROC far ; New int 8 handler
- cli ; Make sure
- push ds
- push cs
- pop ds ; Address this segment with DS
- ASSUME ds:Code
- push dx
- push cx
- push bx
- push ax
- pushf
- cld ; Ensure DF is known
-
- ; Read count in progress in the CTC to CX
-
- xor al,al ; Command to latch channel 0
- out 43h,al
- jmp SHORT $+2 ; Short delay
- in al,40h ; Read lobyte of count in progress
- jmp SHORT $+2 ; Short delay
- xchg ax,cx ; To CL
- in al,40h ; Read hibyte of count in progress
- mov ch,al ; To CH - now CX = count in progress
-
- ; Now have count in progress, in CX. Calculate the latency on this interrupt
- ; invocation. This can be determined from the reload value last programmed
- ; into the CTC (which is stored in LastCTC). The difference between LastCTC
- ; and the count in progress, is the latency. This value is left in CX.
- ; If reporting, update the MaxLatency variable if appropriate.
-
- neg cx ; Convert count in progress to negative
- add cx,[LastCTC] ; Now have latency for this interrupt
- mov [Latency],cx ; Store as measured latency
- IF REPORT
- cmp cx,[MaxLatency] ; Update MaxLatency
- jbe NotWorse ; If not exceeded current value
- mov [MaxLatency],cx ; If exceeded, update
- ENDIF
-
- ; Check for this interrupt handler being entered too late. This occurs if a
- ; retrace was already in progress when the interrupt routine was entered, or
- ; if the measured latency is significantly greater than the SafeMargin value
- ; (at least, say, 10 or 20 CTC clocks later, to allow for timing alignment
- ; errors - I have chosen 20 CTC clocks; anything less than this will always
- ; be picked up by the retrace being already active).
- ;
- ; If this occurs, the interrupt was delayed longer than the SafeMargin value,
- ; and the start of the retrace interval (and possibly the whole retrace pulse)
- ; has been missed.
- ;
- ; The logic is that either the interrupt was accepted in time, in which case
- ; we will wait for the start of retrace and reset the CTC with the correct
- ; delay again, or the interrupt was delayed past the start of retrace (and
- ; possibly even past the end of retrace!) In this case, we must find out
- ; how 'late' we are, not wait for the start of retrace (as it has already
- ; started and may even have finished), and program the CTC with an adjusted
- ; delay (adjusted downwards), so that the next interrupt will be signalled
- ; on schedule.
- ; This technique does not resynchronise the CTC interrupt to the video system,
- ; and does not include compensation for the delays in the code, so if retraces
- ; are missed repeatedly, the timing of the interrupts is likely to drift.
- ; After the first successful interrupt entry, however, the CTC will be
- ; resynchronised to the video retrace.
-
- NotWorse: xor bx,bx ; Zero loop counter / flag for later
- mov dx,3DAh ; VGA status port
- in al,dx ; Get status
- test al,00001000b ; Check for retrace
- jnz MissedRetrace ; If active, we missed the start
- mov ax,[SafeMargin] ; Get ideal interrupt acceptance delay
- add ax,20 ; Get maximum expected safety window
- cmp cx,ax ; Measured interrupt acceptance delay
- jae MissedRetrace ; Oh dear, we missed it completely!
-
- ; The latency (interrupt acceptance delay) was comfortably smaller than
- ; SafeMargin, and retrace is not active, so presumably the interrupt was
- ; accepted within the safe period. We can now wait for the retrace to
- ; start, then reprogram the CTC with the standard delay (from FieldPeriod
- ; minus SafeMargin). In report mode, count the loops while waiting.
-
- Retrace1: IF REPORT
- cmp bx,0FFFFh ; Check for overflow
- adc bx,0 ; Increment if not
- ENDIF
- in al,dx ; Read status
- test al,00001000b ; Check retrace flag
- jz Retrace1 ; If clear, keep waiting for retrace
-
- ; We have just successfully completed the wait-for-retrace loop. If in report
- ; mode, update longest and shortest wait times but only if this is not the
- ; first retrace.
-
- IF REPORT
- cmp [First],0 ; Is it the first retrace?
- jnz IsFirst ; If so
- cmp bx,[Longest] ; Longer than longest?
- jbe NotLonger ; If not
- mov [Longest],bx ; If so
- NotLonger: cmp bx,[Shortest] ; Shorter than shortest?
- jae NotShorter ; If not
- mov [Shortest],bx ; If so
- IsFirst: mov [First],0 ; Reset First flag if set
- NotShorter: ELSE
- inc bx ; Flag that retrace was safe
- ENDIF
- mov cx,[FieldPeriod] ; Total field time minus safe margin
- sub cx,[SafeMargin] ; Prepare value to load into CTC
- jmp SHORT ResetCTC ; Go to set up CTC
-
- ; We missed the start of retrace because the interrupt was delayed, probably
- ; by some foreground code locking interrupts out for a long time. This can't
- ; be helped now, but we must adjust the value programmed into the CTC to
- ; trigger the next interrupt, so that it will interrupt proportionally sooner,
- ; otherwise we will miss the next retrace, etc.
- ; Calculate the new value to be programmed into the CTC. This is simply the
- ; retrace period (FieldPeriod) minus the measured latency, which is already in
- ; CX from earlier calculations. This gives an adjusted value to load into the
- ; CTC for the next delay, so that it will interrupt at roughly the correct
- ; point next time.
-
- MissedRetrace: IF REPORT
- inc [MissedStarts] ; Flag we missed a retrace start
- ENDIF
- neg cx ; Get minus interrupt acceptance delay
- add cx,[FieldPeriod] ; Get adjusted CTC load value
-
- ; At this point, we have either missed the start of retrace and calculated a
- ; reduced value to load into the CTC for the next delay, or we have just had
- ; the start of retrace and have the standard value (FieldPeriod - SafeMargin)
- ; to load into CTC channel 0. CX contains the value to be loaded into the CTC
- ; to determine the delay from now until the next interrupt is signalled.
- ; Reset and restart the CTC using this value.
-
- ResetCTC: mov [LastCTC],cx ; Store as last programmed value
- mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin
- out 43h,al ; Prepare channel 0 for new divisor
- jmp SHORT $+2 ; Short delay
- xchg ax,cx ; Get CTC count value
- out 40h,al ; Write lobyte of divisor
- jmp SHORT $+2 ; Short delay
- mov al,ah ; Hibyte of divisor
- out 40h,al ; Write hibyte of divisor
-
- ; Set carry flag according to whether retrace missed, and call RetraceFunc.
- ; BX was cleared before the test for retrace already started, so if retrace
- ; had already started (i.e. retrace start missed), BX will still be zero.
- ; If not, BX will be at least 1, as it is incremented in the wait loop (if
- ; reporting is enabled) or explicitly (if reporting is not enabled).
-
- cmp bx,1 ; Set carry if retrace had started
- call RetraceFunc ; Do retrace stuff
-
- ; Increment retrace count (but not above 0FFFFh)
-
- IF REPORT
- cmp [Retraces],0FFFFh ; Check for overflow
- adc [Retraces],0 ; Increment if not
- ENDIF
-
- ; Either chain to BIOS int 8 handler, or send EOI to PIC and return from
- ; interrupt. The decision is made via the Int8Sched variable, which is
- ; incremented by the number of CTC clocks in each field (FieldPeriod).
- ; If it carries, the BIOS int 8 handler is called. Otherwise, we just send
- ; an EOI and return from interrupt.
- ; In production code, this logic could be modified to remove the Int8Sched and
- ; the conditional chain to the BIOS int 8 handler, and always send the EOI and
- ; return. If this is done, the system time will stop updating while the handler
- ; is installed. There is little to be gained by doing this, as the interrupt
- ; rate is not very high, so I suggest leaving the chaining code intact.
-
- mov ax,[FieldPeriod] ; Get number of CTC clocks elapsed
- add [Int8Sched],ax ; Add into BIOS int 8 scheduler variable
- cli ; Don't allow stack growth
- jc CallOld8 ; If it carried, chain to the BIOS
- mov al,20h ; EOI command
- out 20h,al ; Send to primary PIC
- popf ; Restore registers
- pop ax
- pop bx
- pop cx
- pop dx
- pop ds
- ASSUME ds:nothing
- iret
- CallOld8: popf ; Restore registers
- pop ax
- pop bx
- pop cx
- pop dx
- pop ds
- DB 0EAh ; JMP xxxx:xxxx
- Old8Ofs DW 0 ; Offset of BIOS int 8 handler
- Old8Seg DW 0 ; Segment of BIOS int 8 handler
- New8 ENDP
-
- ; RetraceFunc is called by the replacement int 8 handler on every retrace, just
- ; shortly after the start of retrace (unless the start of retrace was missed,
- ; see shortly). On entry, the main segment is addressable via DS, the direction
- ; flag is clear, and interrupts are disabled - they may be enabled within
- ; RetraceFunc, but since IRQ0 (the highest priority interrupt) is in progress,
- ; no other interrupt sources will get through anyway, so there is no point in
- ; issuing an STI. The flags and the four scratchpad registers (AX, BX, CX, and
- ; DX) may be destroyed, but any other registers must be preserved - specifically
- ; BP, SI, DI, and ES must be preserved. The int 8 handler does not perform a
- ; stack switch, so stack usage must be kept to a minimum. On entry, the carry
- ; flag indicates whether a full retrace period is available, and the Latency
- ; variable can be used to determine how much time remains if the full period
- ; is not available.
- ; The timer interrupt normally triggers a certain time (set by SafeMargin)
- ; prior to the start of a retrace, giving a safety margin in case interrupts
- ; are locked out and the timer interrupt is not actioned immediately when it
- ; is signalled by CTC channel 0. If interrupts are locked out for more than
- ; the safety margin period, the timer interrupt may be delayed until after
- ; the start of retrace, possibly even until after the end of the retrace pulse.
- ; The int 8 handler detects this condition, and sets the carry flag on entry to
- ; RetraceFunc if this occurred. Normally, carry will be clear on entry to
- ; RetraceFunc.
- ; This function may make use of the Latency variable, which contains the
- ; number of CTC clocks by which the current interrupt was delayed. Typically
- ; this will be in the order of 15 to 20, but it will be much higher if this
- ; interrupt entry was delayed by interrupts being disabled by foreground
- ; code. If the time between the start of retrace and the start of the next
- ; visible scan is known, it is possible to use the Latency variable to find
- ; the amount of time remaining before the visible scan starts.
- ; See the explanatory text for this program for more details.
- ;
- ; This function would be changed to a user-specific function.
- ;
- ; This function must obey the normal guidelines for hardware interrupt handlers,
- ; for example it must not try to call any DOS functions. Some BIOS functions
- ; are generally safe to call from hardware interrupt handlers, but in general,
- ; special operations such as page flipping, palette changing, font programming,
- ; etc should be done at a hardware level, and the mainline code should be aware
- ; that these operations are being done 'in the background' if there may be some
- ; interaction.
-
- RetraceFunc PROC near
- pushf ; Preserve carry flag
- mov bx,[Cycler] ; Get current point
- inc bx ; Bump offset
- and bx,3Fh ; Mask
- mov [Cycler],bx ; Store back
- popf ; Restore carry flag
- jc DontUpdate ; If missed start of retrace
- mov ah,[CycleTbl+bx] ; Get position for this cycle step
- mov dx,3D4h ; Address VGA CRTC
- mov al,8 ; Register number
- out dx,ax ; Set vertical start position
- DontUpdate: ret
- RetraceFunc ENDP
-
- DB 256 DUP(?) ; Stack space
- StackTop = $ ; Top of stack point
-
- FreeSpace = $ ; End of memory required
-
- Code ENDS
- END Main
- -------------------------------- snip snip snip --------------------------------
-
- ## 10.16.3 DOUBLE AND TRIPLE BUFFERING
-
- Thanks to Paul Ross (pa-ross@uwe.ac.uk) for his help with this subject.
-
- Double buffering uses two screen buffers. While one buffer is being displayed,
- the other buffer is being updated with data for the next frame. The video card
- is told to change to the other buffer only during a vertical retrace, so the
- animation is smooth and flicker-free.
-
- The general flow is as follows:
- while (1) {
- Generate next frame using currently non-displayed buffer;
- Wait for vertical retrace to begin;
- Tell video card to swap to other buffer;
- }
-
- There is no requirement for a vertical retrace interrupt, because the software
- simply creates a buffer of data then waits until the retrace starts, then
- flips pages and starts creating the next buffer, and so on.
-
- If one frame of picture data can be generated in less than one vertical scan,
- the buffer alternates every retrace, and the frame update rate is equal to the
- vertical scan rate, i.e. 70 frames per second (or whatever). The software
- wastes time waiting for the vertical retrace, but if the software is still able
- to keep up with the maximum frame display rate of the video hardware, this is
- not a problem. But if it takes slightly longer than one frame to generate the
- next frame, a lot of time is wasted in the loop waiting for retrace.
-
- These diagrams may make more sense (if viewed on a monospaced display).
- The first diagram shows the software able to generate a new frame more
- quickly than the video card's frame rate:
-
- Retraces: ! ! ! !
- Software: 1111111111wwwwwf2222222222wwwwwf1111111111wwwwwf22222...
- Display: 222222222222222 111111111111111 222222222222222 11111...
-
- Key: 1 = Generating data for, or displaying, buffer 1
- 2 = Generating data for, or displaying, buffer 2
- w = Waiting for vertical retrace
- f = Flipping pages on video card
-
- The above diagram showed the buffers alternating every retrace, giving the
- maximum displayable frame update rate. The next diagram shows what happens
- with double buffering when it takes longer than one displayed frame for the
- software to create data for the next frame:
-
- Retraces: ! ! ! ! ! ! !
- Software: 1111111111wwwwwf2222222222wwwwwf1111111111wwwwwf22222...
- Display: 2222222 2222222 1111111 1111111 2222222 2222222 11111...
-
- As you can see, if the software is too slow to keep up with the frame rate of
- the video card (as is often the case), the same frame will be displayed twice
- or three times (or whatever), while the software is creating the next frame.
- Once the software has a new frame ready, it then starts _waiting for the start
- of the next frame_, wasting up to nearly a whole frame time doing nothing.
-
- If the code takes, say, 1.3 frames to generate a picture, it will always flip
- pages every two frames, because it can't do anything while waiting for the
- retrace. So the screen updates are always evenly spaced (assuming that each
- frame takes the same amount of time to generate), but if you could use that
- waiting time, you could actually flip pages on two out of every three frames,
- like this:
-
- Retraces: ! ! ! ! ! ! !
- Software: 111111111122222f2222233f3333333f111111111222222222333...
- Display: 3333333 3333333 1111111 2222222 3333333 3333333 11111...
- Comments: ^1ready ^2ready ^3ready ^1ready
-
- So you get the page flips spaced unevenly, but the frame rate goes up.
-
- This is called triple buffering, and it helps by allowing you to get a higher
- frame rate but with uneven frame timing. For example if it takes 1.3 frames
- of time to generate a new frame of data, with double buffering you would spend
- 0.7 frames every two frames (i.e. 35% of the processor time) waiting for the
- next retrace, so you would get one new frame of data every two scans, i.e. 35
- fps. But with triple buffering, you could start creating a new frame during
- that 0.7 of a frame, so that it could be ready sooner. So you would get
- unevenly spaced frames, but a higher frame rate.
-
- To implement triple buffering, first of all you must use a video mode where
- three (or more) pages are available. Then to detect retrace, you can either
- poll the card (but remember the retrace pulse may be only about 64 us wide!),
- or use some method based on polling the CTC, or use the vertical retrace
- interrupt or an emulated vertical retrace interrupt.
-
- Using the vertical retrace interrupt or emulated vertical retrace interrupt
- method, your mainline (frame data generation) must keep some variable to show
- which frame contains the most recent valid data, and a flag to say that a new
- frame is available, which could be combined with the other variable. The
- interrupt routine, which is triggered every retrace, would then check to see
- if a new frame is available, and if so, flip pages to enable the most recently
- updated page to be displayed.
-
- So the mainline code flow would be something like:
-
- Set newframe variable to -1;
- Set workframe variable to 0;
- while (1) {
- Generate frame in buffer specified by workframe variable;
- Set newframe variable to workframe variable;
- Increment workframe variable modulo 3 (0,1,2,0,1,2,0...);
- }
-
- And the vertical retrace interrupt handler flow would be:
-
- If newframe variable is not -1 { /* Only flip if new data available */
- Flip displayed page to be equal to newframe variable;
- set newframe variable to -1;
- }
-
- ## 11 QUESTIONS AND ANSWERS
-
- Well, since this is supposed to be a FAQ, I suppose I should include some
- frequently asked questions and my answers to them :-) Most of these questions
- are from Usenet newsgroups alt.msdos.programmer, comp.os.msdos.programmer,
- comp.lang.asm.x86, and related newsgroups. I have paraphrased most of them.
-
- ## 11.1 TIMING ACCURACY
-
- ----------
- > What is the inherent inaccuracy in DOS's timekeeping and how can it be
- > avoided in an application where long term time accuracy is important?
-
- There are 1,573,042.24 ticks in a day, but when the BIOS was written, the
- 1.19318166666... MHz frequency was approximated to 1.193180 MHz, so the BIOS
- writers used 1,573,040 (001800B0 hex) ticks per day. This contributes a
- 'by-design' error of 1.42166 parts per million, but this is swamped by the
- error due to initial accuracy, temperature stability, and long term drift in
- the 14.31818 MHz system clock crystal, which is 5 ppm for a good quality
- crystal, and maybe 50 ppm for the crap ones that are often found in cheap PCs.
-
- One solution would be to write a DOS device driver for the CLOCK$ device, which
- accesses the RTC (either directly, or through the BIOS functions), so that the
- DOS time no longer relates to the time maintained by the BIOS in the timer tick
- count variable.
-
- However, the errors (initial, temperature, and long term) in the clock frequency
- of the RTC are probably going to be unacceptable also, unless your motherboard
- has a trimmer capacitor to fine-tune the oscillator frequency, and you have
- _lots_ of time to spend adjusting it :-) Also the RTC only has a resolution
- of one second.
-
- If you really need high accuracy, there are several approaches.
-
- ■ Measure the accuracy over a one day or one week period and install an
- adjustment factor in the software to compensate for the initial frequency
- error (has to be done individually for each machine that will run the
- software). This method doesn't help against temperature and long-term
- drift.
-
- ■ Install a more accurate crystal or a high quality crystal oscillator module.
-
- ■ Use an external frequency source - either a clock controlled from a high
- quality crystal in a temperature controlled environment (crystal oven) or
- something derived from an external clock source (such as the mains frequency,
- or perhaps radio time signals?), into an input such as the parallel port ACK
- line which can generate an interrupt.
-
- ----------
- > I want to implement a 10 millisecond clock, i.e. an interrupt every 10 ms.
- > The PIT clock is 1.19318 Mhz, so a count of 11931 will give an interrupt
- > at a rate of 11931/1193180 = 9.99933 ms. Using a divisor of 11931, I counted
- > interrupts over a long period and got 9.99849 ms per tick.
-
- The PIT clock is 14.31818/12, or 1.19318166666.... MHz. The absolute accuracy
- is normally better than +/- 100 ppm, often under +/- 10 ppm, depending on the
- accuracy of the crystal. Modern motherboards may not use accurate crystals,
- because there is not normally any reason to - the RTC determines the long-term
- accuracy and this is is clocked separately and read on every reboot. Try again
- using the correct value for the timer clock frequency - this should give a
- closer result, but you may not be able to get the accuracy you need.
-
- > I tried a count of 11932. This should give a tick interval of greater
- > than 10 ms, but instead, I get the 9.99933 ms tick interval I expected
- > with a count of 11931. Even worse, all of this happens only on some
- > machines; others work as expected.
-
- Clock frequencies vary from machine to machine, also with age and temperature,
- again depending on the quality of the crystal used. If the program just has
- to run on one machine, and its clock frequency is slightly off but at least
- stable, you may be able to calculate the actual clock frequency and modify
- the program to accommodate it.
-
- Also, you can get non-integer division by alternating or cycling the reload
- value between two different numbers, e.g. using 11930 on one cycle then 11931
- on the next to get 11930.5 (long term, that is :-)
-
- One more thing - are you maintaining the BIOS timer tick interrupt? It is
- supposed to be called every 65536 clocks. You can use a 16-bit scheduler
- variable, and on every 10ms interrupt, add 11930 (or whatever you used) to the
- scheduler variable, and when the add causes a carry, 65536 clocks have elapsed
- so you should chain to the old int 8 handler rather than sending an EOI and
- returning.
-
- ## 11.2 TIMER INTERRUPTS (INT 8, INT 1CH, RTC INTERRUPT)
-
- ----------
- > If there are no TSRs hooking into it, what does the timer-tick interrupt do
- > other than being used for counting the number of ticks since midnight?
-
- The traditional functions of int 8 (the hardware timer tick interrupt) are (a)
- updating the BIOS tick count variable which is used by DOS to determine the
- time of day, and setting the midnight flag if a midnight has passed, and (b)
- turning off the floppy drives after about two seconds since the last access.
-
- BIOSes _may_ use int 8 for anything else that they like. For example they
- _could_ use int 8 for green functions (e.g. spinning down the hard drive if
- it has not been accessed for a while on a laptop or killing the video drive
- if no video accesses have been made). I am not saying that BIOSes _do_ this,
- just that it is their perogative to do this, so it's not safe to assume that
- int 8 is only used to update the tick count and turn off the floppy drives.
-
- Also, any number of TSRs and device drivers, such as screen savers and disk
- caches, could be using int 8 and/or int 1Ch.
-
- ----------
- > I have seen TSRs that hook int 1Ch rather than int 8, this implies that an
- > application program should chain to the previous handler if it uses int
- > 1Ch unless it has a good reason not to do so.
-
- My understanding is that int 1Ch is intended for use by user programs only, and
- that it should be neither necessary, nor desirable, to chain to the original
- handler, as the original handler is just an IRET. The user program's only
- obligation should be to restore the vector when it terminates. However, some
- TSR writers obviously didn't think this way (or maybe just didn't _think_ :-)
- so there are TSRs that hook int 1Ch. For their benefit your application can
- and should chain int 1Ch. But I do not believe TSRs should use int 1Ch.
-
- ----------
- > What are the advantages of using int 8 versus int 1Ch? Documents I've read
- > recommend using int 1Ch. Why would you use int 8 instead?
-
- It depends what you want to do with the interrupt. If you just want a 54.9254
- ms regular interrupt in an application program (i.e. not a TSR), you can use
- either.
-
- If you are writing a TSR, you should use int 8, not int 1Ch, because int 1Ch is
- intended for use by user programs, and a TSR is not a user program, it is more
- like an operating system extension, and a user program is within its rights to
- come along and hook int 1Ch without chaining to your handler. In this case
- (using int 8 in a TSR), you must chain to the original handler. The simplest
- way is just to JMP to it at the end of your intercepting code.
-
- If you are modifying the timer tick rate, or doing vertical retrace emulation,
- or anything clever with the timer, you must use int 8, and ensure that the old
- int 8 handler is chained at appropriate intervals. This technique cannot safely
- be used inside a TSR because an application is at liberty to pull the same
- tricks and break the TSR.
-
- In all cases, keep the amount of time spent in the interrupt handler to a
- minimum.
-
- ----------
- > How can I increment a variable once every second, under interrupt?
-
- Timed interrupts on the PC can be generated via channel 0 of the timer chip
- (8253 or 8254) and via the real time clock (RTC).
-
- The timer cannot generate interrupts at one second intervals. It is normally
- operating at 18.2065 interrupts per second (this is called the 'timer tick').
- You can hook into this timer tick interrupt (int 8 or int 1Ch if you're writing
- an application, int 8 only if you're writing a TSR). You can then count off
- interrupts and increment your seconds counter every 18.2065 interrupts. This
- is done by incrementing it after 18 or 19 interrupts, and alternating between
- 18 interrupts between increments, and 19 interrupts between increments, to give
- over the medium term or long term, one increment every 18.2065 interrupts.
- This requires some simple arithmetic. Of course this will cause the seconds
- variable to be incremented slightly unevenly. If that's acceptable, this is
- probably the best way to go. This technique can be used in an application or
- a TSR.
-
- If a slight unevenness in timing is not acceptable, you can reprogram timer
- channel 0 to operate at a different rate, such as, say, 50 ticks per second,
- and hook int 8, and call the old int 8 handler ('chain') 18.2065 times per
- second. The timer cannot generate exactly 50 interrupts per second with a
- single divisor value, but this can be achieved by dynamically reloading the
- timer divisor on each interrupt. Of course this method makes the calls to
- the old int 8 handler uneven, but this is not a problem for the software that
- uses this interrupt. You then can count off 50 fast interrupts and increment
- your seconds variable. However, this technique cannot safely be used in a TSR.
-
- The above techniques use the timer (8253/8254). If you know your program will
- always run on an AT or later, you can use the RTC. It is able to generate an
- interrupt every second, but this mode is not normally used. I've never tried
- using the update interrupt (once per second) but it should work, provided that
- you use the normal tricks to make sure the BIOS doesn't turn off the interrupt
- source. Alternatively, you could use the RTC at 1024 interrupts per second and
- count off the interrupts yourself. This technique will definitely work, though
- you are more likely to miss interrupts because they are happening at a faster
- rate.
-
- ----------
- > What is the difference, and interaction, between the timer tick interrupt
- > and the real time clock's periodic interrupt?
-
- The timer interrupt is triggered by channel 0 of the timer chip, an Intel 8253
- or 8254 or workalike. It is normally operated at 18.2065 interrupts per second
- (this is called the timer tick rate). The default handler is responsible for
- maintaining the system time (which is done through the BIOS Tick Count Variable
- in the BIOS Data Area) and turning off the floppy drive motors after two seconds
- of inactivity, and it is likely that some machines use it for other purposes
- too. The timer tick interrupt is IRQ0, which is int 8. This is the highest
- priority hardware interrupt request. As well as updating the time and turning
- off the floppy drive motors, the default handler issues int 1Ch on every tick.
- This interrupt is intended for use by user programs (not TSRs) as a regular
- interrupt source. TSRs and network software often intercept int 8 and use it
- for timing, timeout detection, regular updating, etc etc. The timer interrupt
- can be operated at a higher rate if desired (this technique cannot be used in a
- TSR). It can be programmed to occur at 1.19318166666...MHz divided by any
- integer from 2 to 65536 (very small divisors cause major overhead problems!).
- With trickery (the dynamic timer tick technique) it can be made to occur at a
- convenient rate, e.g. 1000 interrupts per second, etc. The program operating
- the timer at a higher rate must chain to the original handler at the correct
- rate, i.e. 18.2065 times per second.
- The timer and int 8 are present in all PC-compatible machines.
-
- The RTC is only present in the AT and later machines, which these days is at
- least 99% of the market. It is connected to IRQ8, which is int 70h. This is
- the highest priority interrupt on the slave interrupt controller, so it is
- third highest priority on the machine (highest and second highest are int 8,
- the timer tick, and int 9, the keyboard scancode interrupt). IRQ8 interrupt
- is generated by the RTC (Real Time Clock) chip, which also holds the machine's
- CMOS memory for storing BIOS settings. The interrupt can be programmed to
- occur at a particular time (through the Alarm function of the RTC), every
- second (the 'update' interrupt), or at one of the following rates (the periodic
- interrupt) - 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, or 8192
- interrupts per second. The RTC interrupt is used by several BIOS functions,
- and the BIOS interrupt handler will sometimes turn off the interrupt, so when
- you hook this interrupt, you have to chain to the BIOS's handler then turn the
- interrupt source back on, just in case.
-
- ## 11.3 INTERRUPT PRIORITIES AND NESTING
-
- ----------
- > While an interrupt handler is in progress, if the interrupt flag is cleared,
- > can the handler be interrupted by another hardware interrupt? Also, if an
- > interrupt handler takes a long time to run, can it be interrupted by itself
- > (for example, a keyboard interrupt handler)?
-
- No hardware interrupt will be accepted if the interrupt flag is clear, as it
- is on entry to an interrupt handler. But, it is normal for most interrupt
- handlers to enable interrupts via an STI instruction fairly early on, so that
- higher priority interrupts will be able to interrupt them.
-
- Hardware interrupts are prioritised by the 8259 interrupt controller(s).
- Lower IRQ numbers are higher priority (unless software has reprogrammed the
- interrupt controller modes). IRQ8-15 (not present on original PC and XT) fit
- in between IRQ1 and IRQ3. In other words, the priority order is:
-
- IRQ0 INT 8 Timer tick interrupt (HIGHEST PRIORITY)
- IRQ1 INT 9 Keyboard scancode interrupt
- IRQ2 INT 0Ah Uncommitted (see IRQ9) (ONLY PRESENT ON ORIGINAL PC AND XT)
- IRQ8 INT 70h RTC interrupt
- IRQ9 INT 71h Redirected IRQ2, uncommitted (COM ports, vertical retrace)
- IRQ10 INT 72h Unallocated
- IRQ11 INT 73h Unallocated
- IRQ12 INT 74h Bus mouse hardware interrupt
- IRQ13 INT 75h Math coprocessor
- IRQ14 INT 76h Hard disk (AT and later)
- IRQ15 INT 77h Unallocated
- IRQ3 INT 0Bh COM2, usually, or uncommitted
- IRQ4 INT 0Ch COM1, usually
- IRQ5 INT 0Dh Uncommitted (COM ports, sound cards, XT hard disk)
- IRQ6 INT 0Eh Floppy disk hardware interrupt
- IRQ7 INT 0Fh Parallel port, sound cards (LOWEST PRIORITY)
-
- If an interrupt handler is in progress, say on IRQ4 for example, and the handler
- enables interrupts using STI, then a higher priority interrupt, such as the
- keyboard scancode interrupt or timer tick interrupt, if signalled, _will_
- interrupt the interrupt handler in progress. Once that higher priority
- interrupt has been processed, the lower priority interrupt handler will be
- resumed. But, a lower or equal priority interrupt will _not_ interrupt the
- handler in progress, until that handler has sent an EOI (end of interrupt)
- command to the interrupt controller(s) (first interrupt controller for IRQ0-7,
- both interrupt controllers for IRQ8-15). The EOI command is value 20h, and is
- sent to I/O port 20h for the first interrupt controller, and port 0A0h for the
- second interrupt controller. The EOI command tells the interrupt controller
- that the interrupt that was signalled by the interrupt controller (which
- provides the interrupt vector to tell the processor where the interrupt handler
- begins) is now finished with, and it resets the interrupt controller's logic.
- The interrupt controller will then signal any other interrupts that were
- pending. For example if an IRQ7 came along while IRQ4 was being processed,
- the interrupt controller would ignore it until the IRQ4 handler issued an EOI,
- then the interrupt controller would re-evaluate its pending interrupts and
- issue the highest priority pending interrupt, which would be IRQ7.
- To avoid the lower priority interrupt 'nesting' on top of the higher priority
- interrupt and causing stack growth, the EOI command is normally issued right
- at the end of the interrupt handler, and is issued with interrupts locked out,
- so that the interrupt handler will return to the main code before the lower
- priority interrupt is accepted.
-
- If you specifically want a particular interrupt priority to be able to interrupt
- itself, this is normally possible - just send the EOI at an early stage in the
- interrupt handler, and make sure interrupts are enabled. The interrupt
- controller thinks the interrupt handler has finished, so it will signal the
- interrupt again if the interrupt is triggered again during processing of the
- same priority interrupt. Of course this also lets lower priority interrupts
- through as well.
-
- --------
- > The timer triggers IRQ0 (int 8) which has highest priority. Does this mean
- > that it really interrupts another lower priority interrupt, or does it only
- > mean that if there are several interrupts pending, IRQ0 will be chosen?
-
- First, no IRQ will interrupt _anything_ if the interrupt flag in the processor
- is clear (via CLI). This flag is also cleared automatically on entry to any
- interrupt handler, and must be explicitly set by the interrupt handler. Most
- software and hardware interrupt handlers will do this, unless they have some
- special reason for not doing so.
-
- If a lower priority interrupt is in progress, and the interrupt flag is set
- (interrupts are enabled), then a higher priority interrupt _will_ interrupt
- that interrupt handler. When the higher priority interrupt exits, the lower
- priority interrupt handler is resumed, in the normal way. If an interrupt of
- the same or lower priority occurs, it will not be serviced until the current
- interrupt handler has finished and sent an end of interrupt signal (more
- below).
-
- If interrupts are locked out via the interrupt flag in the processor, the
- interrupt controller chip will continually evaluate its inputs, keeping track
- of the highest priority pending input, and when the processor is able to accept
- the interrupt, the interrupt controller will first issue the highest priority
- interrupt.
-
- If a hardware interrupt request disappears while the interrupt controller is
- waiting for the processor to acknowledge its interrupt request (INTR), the
- interrupt controller is in the embarrassing position of having interrupted the
- processor but not having a valid interrupt request to issue. In this case,
- the interrupt controller issues an interrupt level 7. If the fleeting
- interrupt was on the primary interrupt controller (IRQ0, IRQ1, or IRQ3-7),
- this will cause IRQ7 (int 0Fh) to be executed. If the fleeting interrupt was
- on the secondary interrupt controller (IRQ8-15), this will cause IRQ15 (int
- 77h) to be executed. Any program handling IRQ7 and/or IRQ15 should be
- prepared for this possibility.
-
- The interrupt controller keeps track of the current interrupt priority. It
- knows when the interrupt priority changes to a higher priority, because it
- issued the interrupt request itself. It also knows when the higher priority
- interrupt ends, and a lower priority interrupt resumes, via the end of
- interrrupt command.
-
- > What is an EOI (end of interrupt) and what type should I use?
-
- Any hardware interrupt handler must notify the interrupt controller (or
- controllers, if it's IRQ8 or higher) when it has completed, so that the
- interrupt controller can keep track of interrupt levels in progress, etc.
- It does this partly through the EOI (end of interrupt) command. There are
- two types of EOI - the non-specific EOI and the specific EOI. Specific EOI
- is not often used, though any 8259 compatible interrupt controller should
- support it. It simply tells the interrupt controller that a specific interrupt
- handler has finished. The non-specific EOI tells the interrupt controller that
- the currently executing, highest priority interrupt handler has finished. The
- command is sent like this. The interrupt controller knows the highest priority
- executing interrupt level, because it generated the interrupt request and
- provided the vector.
-
- Assembler mov al,20h
- out 20h,al
- Micro$oft C outp(0x20, 0x20);
- Borland C outportb(0x20, 0x20);
- Pascal port[$20]=$20;
- GW-BASIC Just kidding :-)
-
- If the interrupt handler is for IRQ8 or higher, it must send an EOI command
- (0x20) to the secondary interrupt controller, at I/O address 0xA0, as well.
- I don't believe it really matters in what order these EOIs are sent in this
- case.
-
- If you are hooking int 8, then you should chain to the original int 8 handler,
- unless you have a special reason for not doing so. The original int 8 handler
- is part of the BIOS. It will send the EOI for you.
-
- ----------
- > How do I tell if my timer tick interrupt handler is taking too much time, and
- > what would happen if the interrupt was to get called again while the handler
- > was still running from the first time?
-
- Until you send the EOI or chain to the original handler (in the case of int 8)
- or until you return (in the case of int 1Ch), the interrupt will not be called
- again while it is still running.
-
- > How are IRQ2 and IRQ9 related? I have run out of free IRQs except for IRQ2,
- > which I have kept free, since I have IRQ9 in use, and wanted to avoid any
- > problem. Can I use IRQ2?
-
- If you have IRQ9 in use, you are going to have trouble 'using' IRQ2 because the
- slot bus pin that was IRQ2 on the PC and XT is IRQ9 on later machines, so IRQ2
- doesn't 'exist' any more :-)
-
- IRQ2 was just a standard interrupt on the PC and XT, with no assigned purpose
- (often used for extra COM ports or special hardware boards). The AT added a
- second interrupt controller (Intel 8259) which provides IRQ8 through IRQ15
- inputs, but required a 'cascade' interrupt input into the main interrupt
- controller, and IRQ2 was chosen as the cascade interrupt. The slot bus pin
- that used to carry IRQ2 was fed into IRQ9, on the second interrupt controller,
- and BIOS and DOS were modified to software-redirect IRQ9 to IRQ2 so that many
- programs that were able to use IRQ2 would still work properly and be none the
- wiser on ATs when they would really be using IRQ9. The default IRQ9 handler
- sends the EOI to the secondary interrupt controller, then invokes the IRQ2
- handler through the IRQ2 vector. When the IRQ2 handler sends its EOI to the
- primary interrupt controller, the IRQ9 is fully acknowledged.
-
- So that is the relationship between the two interrupts. IRQ2 is not accessible
- on the slot bus on ATs and later machines. This may not apply to MicroChannel
- motherboards, BTW, which were designed after the AT.
-
- ## 11.4 INTERRUPT HANDLER RESTRICTIONS
-
- ----------
- > I'm writing a TSR that will make my computer beep several times when "RING"
- > is received from my modem. I want to make it a hook int 1Ch and make it
- > watch for "RING" using the BIOS serial functions interrupt 14h function 3,
- > then activate the beep. Is it safe to call int 14h from within an int 1Ch
- > handler?
-
- First, TSRs shouldn't use int 1Ch, use int 8 instead, and make sure you chain
- to the original handler. BIOS functions are nominally non-reentrant, but the
- int 14h services are so simple that provided the foreground program isn't using
- them to access the same serial port (very unlikely, as any decent comms software
- goes direct to hardware and doesn't use int 14h at all), you should be safe.
- But if you're using int 14h and calling it from within your int 8 handler, you
- won't call it quickly enough to catch the 'RING' string from the modem - you
- will get a receive overrun, unless you have an internal modem with an emulated
- serial port, which will hold the data until it is read. IOW, you should hook
- the serial interrupt for the serial port you're monitoring, and enable the
- serial interrupt, etc, so you get an interrupt when the modem sends something.
- Finally, how are you planning to program the beep? I suggest going direct to
- hardware, rather than using int 10h, which can often be non-reentrant.
- That means you must turn the sound on and off using the timer interrupt.
-
- ----------
- > When I add a call to puts() in my timer interrupt handler, the machine
- > locks up or crashes with an EMM386 or QEMM exception. Why?
-
- Because puts() calls DOS and DOS is non-reentrant. When the timer tick
- interrupt is signalled, various parts of your computer's software and hardware
- may be 'busy', and calling most DOS functions and some BIOS functions will
- usually cause problems with reentrancy. If you want to output to the screen
- from within an interrupt handler, you either need to use TSR techniques to
- ensure that DOS or the BIOS is not busy, or write directly to screen memory.
- I find the latter technique more useful.
-
- --------
- > Can I save to disk some data which I collect in my interrupt handler?
-
- Yes, absolutely. Especially if it's not a TSR. You can't write to disk from
- your int 8 handler, though - the BIOS might be in the middle of writing
- something else. There are lots of reasons why this would be very dangerous.
- Normally this would be done using a circular buffer, or 'queue', to pass data
- from your interrupt handler to your mainline. You have an area of memory (any
- size from a few bytes up to 32K or so) to be used circularly to store data, and
- have two pointers or offsets into the buffer, one being driven by the interrupt
- routine showing where the data is going in, and one controlled by the mainline
- which runs 'in the foreground' keeping track of data coming out of the circular
- buffer and being written to disk. Every time your interrupt routine puts data
- in the buffer, it 'bumps' the 'ingoing' pointer (increment pointer, check
- whether it has gone off the end of the buffer, and if so, reset it to the start
- of the buffer). Every time your mainline gets data out of the buffer, it bumps
- its outgoing pointer in the same way. If the two pointers are equal, there is
- no new data in the buffer. The interrupt handler should also handle a buffer
- overrun tidily, by checking for the 'ingoing' pointer crossing the 'outgoing'
- pointer and behaving accordingly (e.g. don't update the 'ingoing' pointer, and
- set a global variable somewhere that the mainline can detect, that indicates a
- buffer overflow). The mainline would put the data from the circular buffer in
- to a linear buffer and write that buffer to disk using the standard file I/O
- routines or DOS services when it gets full.
-
- ## 11.5 HIGH SPEED TIMER TICK
-
- ----------
- > I need to trigger an analog to digital converter (which does not have its own
- > clock) 4000 times per second.
-
- This can be done by speeding up the timer tick, see section »» 8 and
- subsections. To get exactly 4000 interrupts per second, you need to use
- the dynamic tick period technique described in section »» 8.6.
-
- ----------
- > I have a 8kbps data stream that I want to capture. I need the computer to
- > synchronise to the data stream. Could I do this in software?
-
- Timer channel 0 can be made to run at 8000 samples per second, but the internal
- timing sources are difficult to synchronise to an external signal. It might
- be possible, but I'd first suggest an external PLL synchronised with the
- signal, triggering interrupts via the ACK pin on a parallel port or through
- a flow control line on a serial port.
-
- ## 11.6 DOS DATE AND TIME
-
- ----------
- > Where and how does DOS store the date?
-
- DOS stores the date as a number of days since 1/1/1980 internally in the CLOCK$
- device driver, which is part of IO.SYS (MS-DOS) or IBMBIO.COM (other DOSes).
- There does not seem to be any way to locate the variable except manually,
- using a debugger.
-
- ----------
- > I am using the RTC to keep the DOS clock in line. Just after midnight, the
- > date counts back a day. If I don't set the DOS clock there is no problem.
- > There is a byte in the BIOS Data Area at 0040:0070 which tells the system
- > that the date rolled over. How does this work?
-
- The midnight flag at 0040:0070 is set to 1 (or just incremented by some BIOSes)
- by the BIOS's int 8 (timer tick) handler when the tick count rolls over from
- 0x001800AF to 0x00000000 (i.e. at midnight).
-
- Every time the BIOS request-tick-count function (int 1Ah with AH=00) is called,
- this flag is returned in AL, and the flag byte in memory is cleared. The flag
- byte is also cleared if the set-tick-count function (int 1Ah with AH=01) is
- called.
-
- DOS relies on this flag when it calls the BIOS function. If your program is
- using the BIOS request-tick-count function, your program will be notified of
- the change of day, but DOS will not, because the flag is cleared as soon as it
- is reported - the BIOS doesn't care whether your program, or DOS, called the
- function, so DOS misses out on seeing the flag, and doesn't increment the date.
-
- In other words, don't use int 1Ah functions 00 and 01, and the DOS date will
- update properly. If you want to read or write the tick count, access it
- directly at 0040:006C.
-
- ## 11.7 ACCESSING HARDWARE
-
- ----------
- > How can I read current time without using any BIOS and DOS function calls?
-
- You can access the RTC chip directly. The RTC is not present in the original
- PC and XT and may not be present in non-hardware-compatible machines. The RTC
- also implements the CMOS which stores your BIOS parameter settings, so be
- careful when accessing it! See section »» 7.35. This gives a resolution of
- one second. Also, you can read the BIOS Tick Count variable, but this is not
- in convenient units. See section »» 4.
-
- ----------
- > I have an acquisition card which measures voltage and frequency of
- > electrical signals. This board can be configured to use IRQ2 through
- > IRQ7 (jumper-selectable) and I/O address 300h. How can I access the
- > devices via the I/O space?
-
- The I/O space is accessed via the IN and OUT instructions of the CPU (if you're
- writing in assembly). In C, use inportb() and outportb() (Borland) or inp() and
- outp() (Micro$oft). In Turbo Pascal, use port[]. The x86 processor in the PC
- can address up to 64K of I/O but the PC's I/O space is usually limited to the
- range 0000h - 03FFh because ISA bus I/O cards only decode the bottom 10 bits of
- the address on I/O accesses.
-
- Your card will probably have a CPU-addressable device such as an 8255 (parallel
- I/O chip) or similar, that will provide the interface between the hardware on
- the board, and the software that you write. If you can identify this chip
- (look for the biggest one, usually :-) and get the data sheet on it, you can
- find out how to talk to it. If it's a proprietary ASIC, though, you might be
- on your own. You could try asking the manufacturer nicely. If that fails,
- you could try disassembling any software that came with the card and working
- out what the I/O accesses are doing.
-
- Typically cards like that will occupy 4, 8, 16, or sometimes 32 adjacent I/O
- locations, and AFAIK the I/O space from 0300h to 031Fh is not normally used by
- any standard PC peripheral, so you should be safe putting it there.
-
- > And can I write an interrupt service routine that will perform I/O through
- > port 300h? Is this a normal procedure?
-
- Yes, and yes. The XT bus supports IRQ2-7. IRQ4 and IRQ3 are usually used for
- COM1 and COM2 respectively, IRQ6 is usually used for the floppy disk, and IRQ7
- is sometimes used for the first parallel port, and for sound cards. IRQ5 is
- also sometimes used by sound cards, and is also often used by the hard drive on
- XTs. Assuming you want to put your XT bus card into an AT, you should be able
- to use IRQ2 or IRQ5 with it, without causing any conflicts. IRQ2 is actually
- remapped to IRQ9 on ATs (long story).
-
- There are several parts involved in setting up an interrupt handler, and I'd
- suggest you first try just talking to the chips on the board, and do the
- interrupt stuff later, as it can get a bit messy.
-
- ----------
- > I need to delay program execution for 1ms. I found some old assembly code
- > that used timer 2 on the PIT. It sets the timer to square wave mode, then
- > counts state changes. This code doesn't work on a Pentium, 486, or 386 PC.
- > How can I make this work on newer PCs? It appears that timer 2 output isn't
- > tied to bit 5 of port 0x62 on these machines. Is there something different
- > about how timer 2 and/or the speaker is implemented on newer PCs?
-
- Yes. The ye olde machines used an 8255 at 60h-63h and the Timer 2 read-back
- signal was on port C at 62h. The AT and later machines use a micro as the
- keyboard interface, and don't implement any port at 62h at all (AFAIK). On
- these machines, Timer 2 readback is on bit 5 of port 61h and operates in the
- same way.
-
- For your purposes, the Refresh Detect signal might be more appropriate. This
- is a read-only signal on bit 4 of the port at 61h, on all machines except the
- old PC and XT, though I wouldn't guarantee it's present on all IBM machines
- (they seem to be the least compatible, for some stupid reason). Anyway, this
- bit toggles state once every 15.0857 microseconds (or 216/14.31818, to be
- exact). This can easily be read in a loop, and you can get a fairly accurate
- delay using this method. It won't work properly if the DRAM refresh rate has
- been changed, but people don't do that much any more :-) This method has
- advantages over using Timer 2 because you can use it with interrupts enabled
- and not have to worry about a keyboard buffer full beep clobbering your timer,
- though of course any interrupts that are serviced during the delay will
- lengthen the delay.
-
- ## 11.8 MISCELLANEOUS
-
- ----------
- > How do I check for a keypress with a timeout?
-
- You need to check for a keypress in a loop, and also incorporate a timeout check
- in the loop. See the sample program in section »» 4.7 and the function in
- section »» 4.8. You can't use getch() or any stream I/O functions, because
- they will wait indefinitely for keys to be pressed. If your compiler supports
- bioskey(1), you can use that, otherwise you can write a function that uses int
- 16h function 1, 11h, or 21h, or int 21h function 6, to poll for a keypress.
-
- ----------
- > How do you create a clock that will run in the top right hand corner of
- > the screen and let the user regain control of the computer in DOS?
- > I think you have to 'hook' an interrupt to accomplish this.
-
- Hook int 8, the timer interrupt (int 1Ch can also be used but is intended
- for use by applications, which may not chain to the old handler, so your
- TSR would stop updating while some apps are active). On every interrupt,
- check the time, either from the BIOS tick count or from the real time clock.
- You can redraw your time on the screen on every int 8 (i.e. 18.2065 times
- per second), or just when the second changes, whichever you prefer. There
- is a sample program in assembler that does this, in section »» 7.35.8.
-
- ----------
- > In my Borland C++ program I need a delay of exactly 100 ms. How can I do it?
-
- There are many ways to implement processor-independent delays on PCs. There
- may be a problem depending on how exact you need your delay to be. There are
- two approaches - wait in a loop for the appropriate length of time, or trigger
- an interrupt at the correct time. With the first approach, you should leave
- interrupts enabled during the loop (otherwise the machine will lose time, as
- the timer tick interrupt comes along every 54.9254 ms), so any interrupts that
- come in during the loop, or near the end of it, may cause your delay to be
- longer than you expected. If you use the other method, acceptance of the
- interrupt can be delayed by foreground code disabling interrupts, or by other
- interrupts occuring during the delay. So there is no ideal solution if you
- need a very accurate delay. If you can tolerate an error of, say, 1% to 5%,
- and your program just wants to wait without doing anything else, and the
- program will not need to run on old PC and XT machines, you can use the Refresh
- Detect delay method, which is pretty tidy. If you can tolerate a resolution of
- 1ms, you can use the BIOS delay functions (not supported on old PC and XT
- machines either). Otherwise you can use the interrupt method, which is fairly
- tricky to program, or a method based on timer 2 (normally used for making beeps)
- which has disadvantages too.
-
- ## 12 REFERENCES
-
- These references are mostly from {JAM} John Mertus's article. He has given me
- permission to include them. I have included comments on the subject matter of
- the books where appropriate. They are in author order. There is no guarantee
- that the books are still available, or that these are the latest editions.
-
- Title: Assembly Language Programming for the IBM Personal Computer
- Author: David J. Bradley
- Published: Prentice-Hall, 1984
- Comments: Possibly the first book to describe timing by reading the CTC
- May be no longer available
-
- Title: Interrupt List
- Author: Ralf Brown
- Comments: Electronic document available on Internet SimTel mirrors, e.g.
- Oak as ftp://oak.oakland.edu/SimTel/msdos/info/inter*.zip.
- Contains an exhaustive list of interrupt usage (mainly
- software interrupts), DOS data structures, etc, essential!
-
- Title: DOS Programmer's Reference, 3rd Edition
- Author: Terry Dettmann, Jim Kyle, Marcus Johnson
- Published: Que Corporation, 1992
- ISBN: 0-88022-790-7; Library of Congress Catalog No. 91-66203
-
- Title: EGA/VGA - A Programmer's Reference Guide
- Author: Bradley Dyck Kliewer
- Published: Intertext Publications / Multiscience Press, Inc, McGraw-Hill
- Book Company, 11 West 19th Street, New York, NY 10011, 1988
- ISBN: 0-07-035089-2
- Comments: Good as a reference but not recommended for beginners
-
- Title: IBM Personal System/2 Hardware Interface Technical Reference
- Published: IBM Corporation, Boca Raton, Florida, 1990
-
- Title: Technical Reference, Personal Computer XT
- Published: IBM Corporation, Boca Raton, Florida, April 1983
-
- Title: Peripheral Components Data Book
- Published: Intel Corporation, Mt. Prospect, Illinois, 1994
- Comments: Includes full data sheet on 8254, recommended.
- According to information in the data book, it can be ordered
- within USA from Literature Sales, P.O. Box 7641, Mt. Prospect,
- IL 60056-7641 or by phone from USA and Canada on (800) 548-4725
- voice or (708) 296-3699 fax, Intel order number 296467,
- ISBN 1-55512-207-8. They accept credit cards, but you may
- need to complete an order form.
-
- Title: PC Programmer's Guide to Low-Level Functions and Interrupts
- Author: Marcus Johnson
- Published: Sams Publishing, 201 West 103rd Street, Indianapolis, Indiana,
- 46290, 1994.
- Comments: Lots of useful low-level technical info, plus documentation on
- BIOS, DOS, EMS, XMS, DPMI, and other APIs. Disk included.
-
- Title: Accurate Timing under Microsoft Windows without
- reprogramming the System Timer
- Author: Jerry Jongerius
- Published: Microsoft System Journal, 1991
- Comments: Reading the CTC
-
- Title: The MS-DOS Encyclopedia
- Published: Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond,
- Washington 98073-9717, 1988
- ISBN: 1-55615-174-8
- Comments: Includes sections on interrupt-driven communications, TSR
- programming, exception handlers, hardware interrupt handlers,
- and debugging, DOS and BIOS function reference, and usage for
- DOS utilities, highly recommended. [KH]
-
- Title: The Peter Norton Programmer's Guide to the IBM PC
- Author: Peter Norton
- Published: Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond,
- Washington 98073-9717, 1985
- ISBN: 0-914845-46-2, Penguin ISBN 0-14-087-144-6
- Comments: There is a newer edition
-
- Title: The Winn Rosch Hardware Bible
- Author: Winn L. Rosch
- Published: Brady Books, New York, 1989
-
- Title: The IBM Personal Computer from the Inside Out
- Author: Murry Sargent and Richard L. Shoemaker
- Published: Addison-Wesley Publishing Co, Reading, Massachusetts, 1986
-
- Title: Netware, the Professional Reference (second edition)
- Author: Karanjit Siyan
- Published: New Rider Publishers, Carmel, Indiana, 1993
-
- Title: The Waite Group's MS-DOS Developer's Guide (second edition)
- Published: Howard W. Sams & Company, 4300 West 62nd Street, Indianapolis,
- Indiana 46268, 1989
- ISBN: 0-672-22630-8 (Library of Congress Catalog Card 88-62227)
- Comments: Includes info on TSRs, serial port, EGA and VGA, and real-time
- programming.
-
- Title: Programmer's Guide to PC & PS/2 Video Systems
- Author: Richard Wilton
- Published: Microsoft Press, One Microsoft Way, Redmond, Washington
- 98052-6399, 1987
- ISBN: 1-55615-103-9
- Comments: Very readable book, recommended [KH]
-
-
- End of the PC Timing FAQ / Application notes
-
- Please drop me a line if you find this document
- useful, or if you have anything to add.
-
- ----//----